feat(bouncer): stamp auth_time at sign-in and carry it unchanged through refresh (C-CR-02)

- MintAudience stamps auth_time equal to iat; refreshAudience copies the
  presented token's value and never renews it (absent stays absent)
- bouncer.ClaimAuthTime and bouncer.VerifyAuthTime for plugins, with the
  same signature, audience and expiry checks as VerifyClaims
- TestAuthTimeSmoke traces mint, refresh and read
This commit is contained in:
Jakub Zych
2026-10-23 16:13:49 +02:00
parent 60a86677f2
commit c14238b94b
6 changed files with 182 additions and 19 deletions

View File

@@ -58,6 +58,12 @@ Revoked token IDs are kept in a `bouncer.BlacklistStore`:
Logging out is blacklisting the token's `jti`. Setting a user's `TokensValidAfter` to now revokes all their tokens at once, for example after a password change.
### Sign-in time (auth_time)
Every token `bouncer.Mint` or `bouncer.MintAudience` signs carries an `auth_time` claim (`bouncer.ClaimAuthTime`, seconds since the epoch) equal to its `iat`, because every direct mint is a sign-in: a password login, an activation, a social login or an admin login. A refresh copies the presented token's `auth_time` into the rotated token unchanged, and a token minted before the claim existed is refreshed without one.
A check that asks for a fresh sign-in, for example before linking two accounts, compares `auth_time`, never `iat`: a refresh renews `iat` without a password, so a weeks-old session would pass an `iat` check. Read the claim with `bouncer.VerifyAuthTime`, which runs the same signature, audience and expiry checks as `bouncer.VerifyClaims` and reports `ok` false when the token has no `auth_time`; refuse such a token rather than treating it as fresh.
## Cookie transport
A browser frontend can keep the token in an httpOnly cookie instead of JavaScript-readable storage. The cookie attributes come from one block, `auth.cookie.*`, which `bouncer.LoadCookieConfig` reads into a `bouncer.CookieConfig` at boot: