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

@ -42,17 +42,18 @@ func freshAuth(t *testing.T) *authkit.Auth {
DB: db,
Hasher: hasher.NewArgon2id(hasher.DefaultArgon2idParams(), nil),
}, authkit.Config{
JWTSecret: []byte("integration-secret-thirty-two!!!"),
JWTIssuer: "authkit-mw-int",
AccessTokenTTL: 2 * time.Minute,
RefreshTokenTTL: time.Hour,
SessionIdleTTL: time.Hour,
SessionAbsoluteTTL: 24 * time.Hour,
EmailVerifyTTL: time.Hour,
PasswordResetTTL: time.Hour,
MagicLinkTTL: time.Minute,
EmailOTPTTL: time.Minute,
EmailOTPMaxAttempts: 3,
JWTSecret: []byte("integration-secret-thirty-two!!!"),
JWTIssuer: "authkit-mw-int",
AccessTokenTTL: 2 * time.Minute,
RefreshTokenTTL: time.Hour,
RefreshChainAbsoluteTTL: 24 * time.Hour,
SessionIdleTTL: time.Hour,
SessionAbsoluteTTL: 24 * time.Hour,
EmailVerifyTTL: time.Hour,
PasswordResetTTL: time.Hour,
MagicLinkTTL: time.Minute,
EmailOTPTTL: time.Minute,
EmailOTPMaxAttempts: 3,
// Plain HTTP for tests so secure-cookie defaults don't interfere
// with httptest's HTTP server.
SessionCookieSecure: authkit.BoolPtr(false),