feat(auth): store Subsonic API keys hashed; a new key is shown once (M462 #4983)
test-web / test (push) Successful in 2m4s
test-go / test (push) Successful in 2m23s
test-go / integration (push) Successful in 5m28s
release / Build signed APK (releases and dev) (push) Successful in 6m29s
release / Build + push container image (push) Successful in 29s
release / Verify release artifacts (tag releases only) (push) Skipped
test-web / test (push) Successful in 2m4s
test-go / test (push) Successful in 2m23s
test-go / integration (push) Successful in 5m28s
release / Build signed APK (releases and dev) (push) Successful in 6m29s
release / Build + push container image (push) Successful in 29s
release / Verify release artifacts (tag releases only) (push) Skipped
users.api_token held each user's apiKey in plaintext and was looked up by equality, so a leaked row or backup handed out working keys. Migration 0063 replaces it with api_token_hash (sha256, hex), computed in place from the existing keys so every Subsonic client keeps working. The key can no longer be read back: GET /api/me/api-token is gone, and POST returns the new key once. Settings shows it right after Regenerate with a copy button and a "won't be shown again" note. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
-- name: CreateUser :one
|
||||
INSERT INTO users (username, password_hash, api_token, is_admin, display_name)
|
||||
INSERT INTO users (username, password_hash, api_token_hash, is_admin, display_name)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING *;
|
||||
|
||||
@@ -17,7 +17,7 @@ RETURNING *;
|
||||
-- and the second caller's INSERT fails with a unique violation. The
|
||||
-- caller (registration handler) can retry as a regular non-admin in
|
||||
-- that case (or surface a "username taken" error to the user).
|
||||
INSERT INTO users (username, password_hash, api_token, is_admin, display_name)
|
||||
INSERT INTO users (username, password_hash, api_token_hash, is_admin, display_name)
|
||||
VALUES (
|
||||
$1, $2, $3,
|
||||
(SELECT NOT EXISTS (SELECT 1 FROM users)),
|
||||
@@ -28,8 +28,8 @@ RETURNING *;
|
||||
-- name: GetUserByUsername :one
|
||||
SELECT * FROM users WHERE username = $1;
|
||||
|
||||
-- name: GetUserByAPIToken :one
|
||||
SELECT * FROM users WHERE api_token = $1;
|
||||
-- name: GetUserByAPITokenHash :one
|
||||
SELECT * FROM users WHERE api_token_hash = $1;
|
||||
|
||||
-- name: CountUsers :one
|
||||
SELECT count(*) FROM users;
|
||||
@@ -87,7 +87,7 @@ WHERE u.id = $1;
|
||||
-- Admin-driven user creation. Distinct from CreateUser/CreateUserFirstAdminRace:
|
||||
-- the caller (an admin) supplies all five fields explicitly including is_admin,
|
||||
-- so an admin can promote on creation. Used by POST /api/admin/users.
|
||||
INSERT INTO users (username, password_hash, api_token, is_admin, display_name)
|
||||
INSERT INTO users (username, password_hash, api_token_hash, is_admin, display_name)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING *;
|
||||
|
||||
@@ -141,11 +141,11 @@ UPDATE users
|
||||
WHERE id = $1
|
||||
RETURNING *;
|
||||
|
||||
-- name: RegenerateApiToken :one
|
||||
-- name: RegenerateApiToken :exec
|
||||
-- Self-service: caller wants a new API token. Used by the /settings
|
||||
-- API Token card's "Regenerate" button.
|
||||
UPDATE users SET api_token = $2 WHERE id = $1
|
||||
RETURNING *;
|
||||
-- API Token card's "Regenerate" button. Only the hash is stored; the
|
||||
-- handler returns the raw key once.
|
||||
UPDATE users SET api_token_hash = $2 WHERE id = $1;
|
||||
|
||||
-- name: GetUserByEmail :one
|
||||
-- Used by forgot-password lookup. Lowercase comparison both sides
|
||||
|
||||
Reference in New Issue
Block a user