Cap refresh chain lifetime via RefreshChainAbsoluteTTL

Sessions had an absolute cap (created_at + SessionAbsoluteTTL) but the
JWT path only had per-token TTL on the refresh row, letting a
well-behaved client refresh indefinitely. Add chain_started_at to
authkit_tokens, copy it forward on every rotation, and reject in
RefreshJWT when now > chainStartedAt + RefreshChainAbsoluteTTL.
Default 30d, mirroring SessionAbsoluteTTL.

Schema, verifier, queries, model, and integration test updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
juancwu 2026-04-26 23:41:02 +00:00
commit ca5525d4bd
11 changed files with 129 additions and 53 deletions

View file

@ -121,17 +121,17 @@ func buildQueries(t Tables) queries {
// tokens
createToken: `INSERT INTO ` + t.Tokens + `
(hash, kind, user_id, chain_id, consumed_at, attempts_remaining, created_at, expires_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
(hash, kind, user_id, chain_id, chain_started_at, consumed_at, attempts_remaining, created_at, expires_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
consumeToken: `UPDATE ` + t.Tokens + `
SET consumed_at = $1
WHERE kind = $2 AND hash = $3 AND consumed_at IS NULL AND expires_at > $4
RETURNING hash, kind, user_id, chain_id, consumed_at, attempts_remaining, created_at, expires_at`,
getToken: `SELECT hash, kind, user_id, chain_id, consumed_at, attempts_remaining, created_at, expires_at
RETURNING hash, kind, user_id, chain_id, chain_started_at, consumed_at, attempts_remaining, created_at, expires_at`,
getToken: `SELECT hash, kind, user_id, chain_id, chain_started_at, consumed_at, attempts_remaining, created_at, expires_at
FROM ` + t.Tokens + ` WHERE kind = $1 AND hash = $2`,
// getOTPForUser returns the most recent unconsumed, unexpired OTP for
// the user, used to verify a code by hash-comparing client input.
getOTPForUser: `SELECT hash, kind, user_id, chain_id, consumed_at, attempts_remaining, created_at, expires_at
getOTPForUser: `SELECT hash, kind, user_id, chain_id, chain_started_at, consumed_at, attempts_remaining, created_at, expires_at
FROM ` + t.Tokens + `
WHERE kind = $1 AND user_id = $2 AND consumed_at IS NULL AND expires_at > $3
ORDER BY created_at DESC LIMIT 1`,
@ -146,7 +146,7 @@ func buildQueries(t Tables) queries {
consumeOTPByID: `UPDATE ` + t.Tokens + `
SET consumed_at = $1
WHERE kind = $2 AND hash = $3 AND consumed_at IS NULL AND expires_at > $1
RETURNING hash, kind, user_id, chain_id, consumed_at, attempts_remaining, created_at, expires_at`,
RETURNING hash, kind, user_id, chain_id, chain_started_at, consumed_at, attempts_remaining, created_at, expires_at`,
deleteByChain: `DELETE FROM ` + t.Tokens + ` WHERE chain_id = $1`,
deleteExpiredTokens: `DELETE FROM ` + t.Tokens + ` WHERE expires_at <= $1`,