19 Commits

Author SHA1 Message Date
Deluan
0628721006 refactor(api): simplify API v1 auth after dropping access tokens
- Fold Allowed into Expand and drop the ErrInsufficientScope sentinel; the
  gate's scopeError is now the only source of insufficient_scope.
- Replace the two-value authKind with a public flag, inline loadUser, and
  pass the grant to touch.
- Read a declared request body once before validation, so the JSON checks
  and the handler no longer depend on kin-openapi restoring the exact bytes.
- Merge the Service tests into one file and drop specs that only covered
  the removed liveness cache. The revoked-during-password-change spec now
  revokes after the gate authenticates, so it reaches ChangePassword again.

Signed-off-by: Deluan <deluan@navidrome.org>
2026-09-28 21:47:16 -04:00
Deluan
d1b876097f refactor(api): use the grant secret as the API v1 bearer credential
API v1 no longer mints short-lived JWT access tokens. Clients send the grant
secret from POST /auth/login or /auth/setup as `Authorization: Bearer` on
every request.

Every request already looked the grant up in the database, so the JWT gave
no speed or revocation benefit and only added a refresh loop, which early
client authors pushed back on. The grant already is an API key: one per
client sign-in, scoped and revocable. Revocation is now immediate on every
node; the contract promises "within one minute".

Removed: POST /auth/token, the grantAuth scheme, the TokenRequest and
AccessToken schemas, the token_expired problem code, the API v1 JWT signer
and its signing key, the grant liveness cache, and PropertyRepository.PutIfAbsent.
ResolveGrant is now Authenticate.

Short-lived tokens return later only as narrow media tokens for
?access_token= on media URLs, together with the media endpoints.

Signed-off-by: Deluan <deluan@navidrome.org>
2026-09-28 21:05:57 -04:00
Deluan
1b5c68a203 refactor(log): mark secrets on their own line and cover session keys
Mark secret values with log.WithSecrets on a separate line instead of
nesting the call in argument lists. Also mark Last.fm/ListenBrainz session
keys written through SessionKeys.Put and the PasswordEncryptionKey checksum,
which still reached trace logs, and ignore values shorter than 8 characters
so a short plaintext marked after a failed encryption cannot mangle SQL text
or the [REDACTED] marker.
2026-09-28 20:06:28 -04:00
Deluan
ddcc611af3 refactor(log): redact caller-marked secret values in the log hook
Replaces the statement-wide SQL arg redaction from the previous commit,
which hid every arg of property and password writes (user names, emails,
scanner properties) and made troubleshooting harder.

log.WithSecrets marks values on a context, log calls now pass their
context to the logrus entry, and the redaction hook replaces those values
in the message and fields. logSQL logs the real args again; only the
encrypted password, the API v1 key and the JWT secrets are marked.
2026-09-28 20:06:28 -04:00
Deluan
8789561b60 fix(api): consolidate grant deletes and harden API v1 auth after review
- GrantRepository keeps three deletes: DeleteForUser, DeleteStaleEpochs
  (replaces DeleteOtherEpochs and DeleteIfEpoch) and DeleteIdle. Delete(id)
  is gone; the idle path in ResolveGrant now calls DeleteIdle, so a grant
  renewed by another node between the read and the delete survives.
  settleEpoch deletes the user's grants below the snapshot's epoch, which
  is safe outside the transaction because epochs only move forward.
- The "dead grants on an older epoch are only deleted when presented"
  policy note moves from the repository to core ListGrants.
- SQL trace logging no longer prints the args of property writes (signing
  keys) or user password writes, both encrypted with a key that may be the
  public default. The SQL statement is still logged.
- The spec gate rejects JSON body keys that differ from a declared property
  only in case. kin-openapi validates exact names while encoding/json
  decodes case-insensitively, so {"scopes":[],"Scopes":null} minted a
  token with every scope and a "Client" key skipped maxLength.
  It also rejects data after the first JSON value, which the handlers'
  decoder ignores and which let a body skip the alias check.
- The liveness cache trims its eviction log on evict, not only on put, so
  evict-only traffic stays bounded; the floor still drops stale fills.
- createAccessToken, login and setupFirstAdmin declare Cache-Control:
  no-store on their success responses.
2026-09-28 20:06:28 -04:00
Deluan
550e03976c refactor(api): shorten the grant touch comment 2026-09-28 20:06:28 -04:00
Deluan
45d8a7724d test(api): share API v1 test helpers and check scopes in Go
Move the end-to-end call/setup/mint helpers to a suite-level test client
and the apiauth login/mustMint helpers to package level. Replace the
hardcoded vacuum x-scope enum with a Go test that every known scope is
a valid spec Scope; the gate already rejects unknown x-scope values.
2026-09-28 20:06:28 -04:00
Deluan
c479c4f581 refactor(api): tighten API v1 auth internals and first-admin setup
Load the signer lock-free once cached, read the signing key before
generating one, cap the liveness cache at its limit, share one
grantable-scope predicate, and let CreateFirstAdmin open its own locked
transaction with an optional in-transaction follow-up.
2026-09-28 20:06:28 -04:00
Deluan
a41e656706 refactor(api): reuse existing helpers in API v1 auth
Use gg.V, slice.Map, chi's RequestSize, core/auth's encryption key and
ClientIPRateLimiter instead of local copies; share the grant idle expiry
constant and a Grant.LastActivity helper; pass last use as a time value.
2026-09-28 20:06:28 -04:00
Deluan
406a2e9cf4 fix(api): never widen the scopes an access token claims
Authenticate ran token claims through Expand, so a token claiming `all`
would have gained every known scope. Only a holder of the signing key
could mint one, but tokens should carry concrete scopes only. Claims now
go through Allowed, which keeps known scopes (and admin only for admins)
and never expands `all`.
2026-09-28 20:06:28 -04:00
Deluan
3e3b73a9cc fix(api): use a 256-bit API v1 signing key
The HS256 key was 22 base62 characters, about 128 bits, below the 256 bits
RFC 7518 section 3.2 asks for. New keys are 32 bytes from crypto/rand,
hex-encoded before being encrypted and stored. Keys already stored keep
working unchanged.
2026-09-28 20:06:28 -04:00
Deluan
b1cfa932be fix(api): make logout idempotent and hide grants left on a stale epoch
Logout answered an undeclared 404 when its grant was already gone, for
example revoked by another node inside the liveness cache window or by a
concurrent logout. It now treats a missing grant as success and still
evicts the cache entry, so logout always answers 200.

Grants left on an older user epoch (after a password reset through the
existing UI, or a login that raced a password change) are dead but only
deleted when presented. Listing and counting grants now filter on the
user's current epoch, so those grants no longer show up.

dropGrant now deletes before evicting, like RevokeGrant, so a concurrent
cache fill cannot re-cache a grant that is being dropped.
2026-09-28 20:06:28 -04:00
Deluan
71c379ff44 feat(api): add API v1 token checks, grant management and password change 2026-09-28 20:06:27 -04:00
Deluan
e3cf849507 fix(api): retry the signing-key load after a failure and drop the password from issued grants 2026-09-28 20:06:27 -04:00
Deluan
0bbca1b133 feat(api): add API v1 login, setup, grant resolution and token minting 2026-09-28 20:06:27 -04:00
Deluan
8c0ba43e9a fix(api): tolerate small clock skew between nodes on access tokens 2026-09-28 20:06:27 -04:00
Deluan
899cc428fd feat(api): add the API v1 signing key and access-token JWTs 2026-09-28 20:06:27 -04:00
Deluan
7dd8dba12b feat(api): add grant liveness cache 2026-09-28 20:06:27 -04:00
Deluan
21ce7c7115 feat(api): add API v1 scope rules and grant secrets 2026-09-28 20:06:27 -04:00