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:
parent
d3c5367492
commit
ca5525d4bd
11 changed files with 129 additions and 53 deletions
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestIntegration_JWTIssueAuthenticate(t *testing.T) {
|
||||
|
|
@ -65,3 +66,35 @@ func TestIntegration_JWTInvalidPrefix(t *testing.T) {
|
|||
t.Fatalf("expected ErrTokenInvalid for malformed input, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegration_JWTRefreshChainAbsoluteCap(t *testing.T) {
|
||||
a := freshAuth(t)
|
||||
ctx := context.Background()
|
||||
u, err := a.CreateUser(ctx, "cap@example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateUser: %v", err)
|
||||
}
|
||||
// freshAuth sets RefreshChainAbsoluteTTL = 24h. Pin clock so the issue
|
||||
// time is fixed, then advance past the cap on refresh.
|
||||
t0 := time.Now().UTC()
|
||||
a.cfg.Clock = func() time.Time { return t0 }
|
||||
_, refresh1, err := a.IssueJWT(ctx, u.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("IssueJWT: %v", err)
|
||||
}
|
||||
|
||||
// One rotation well within the cap should still work.
|
||||
a.cfg.Clock = func() time.Time { return t0.Add(time.Hour) }
|
||||
_, refresh2, err := a.RefreshJWT(ctx, refresh1)
|
||||
if err != nil {
|
||||
t.Fatalf("RefreshJWT within cap: %v", err)
|
||||
}
|
||||
|
||||
// Advance past the absolute cap. The refresh succeeds at the consume
|
||||
// step (token row is not yet expired by RefreshTokenTTL — only the
|
||||
// chain cap kicks in), but the chain cap rejects the rotation.
|
||||
a.cfg.Clock = func() time.Time { return t0.Add(25 * time.Hour) }
|
||||
if _, _, err := a.RefreshJWT(ctx, refresh2); !errors.Is(err, ErrTokenInvalid) {
|
||||
t.Fatalf("expected ErrTokenInvalid past chain absolute cap, got %v", err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue