✨ feat(auth): JWT secret retention policy + automatic cleanup loop (ADR-0021)
Implements the cleanup half of ADR-0021 (which had only config infrastructure landed). Non-primary expired secrets are removed by a goroutine that runs at auth.jwt.secret_retention.cleanup_interval (default 1h). Primary secret is never removed regardless of expiration — invariant preserved. Changes: - pkg/user/jwt_manager.go : add sync.Mutex protection; add RemoveExpiredSecrets() int and StartCleanupLoop(ctx, interval) methods. Reset() now also cancels any running cleanup goroutine. - pkg/user/auth_service.go : delegate to manager via new AuthService methods StartJWTSecretCleanupLoop and RemoveExpiredJWTSecrets. - pkg/user/user.go : extend AuthService interface accordingly. - pkg/server/server.go Run() : start cleanup loop tied to rootCtx so it stops on graceful shutdown. - pkg/jwt/* : same treatment on the secondary (less-used) implementation for consistency. - adr/0021-jwt-secret-retention-policy.md : Status → Implemented + fix numbering (was incorrectly "10."). Tests: - 4 new unit tests in pkg/user/jwt_manager_test.go covering RemoveExpiredSecrets (expired removed, primary preserved, future kept) and StartCleanupLoop (fires + stops on context cancel). - go test -race ./pkg/user/... passes. - Full BDD suite (auth/config/greet/health/info/jwt) still green. - BDD scenarios at @todo / @skip remain so — they require an admin endpoint /api/v1/admin/jwt/secrets which is explicitly out of scope. Verifier verdict: APPROVE_WITH_NITS — StartCleanupLoop is 34 lines (just over the 30-line guideline); 2 time.Sleeps in TestStartCleanupLoop_FiresAndStops are justified by the goroutine-timing nature of the test.
This commit is contained in:
@@ -24,13 +24,25 @@ type JWTSecret struct {
|
||||
ExpiresAt *time.Time // Optional expiration time
|
||||
}
|
||||
|
||||
// JWTSecretManager manages multiple JWT secrets for rotation
|
||||
// JWTSecretManager manages multiple JWT secrets for rotation.
|
||||
// Secrets can carry an optional expiration; the cleanup loop removes them
|
||||
// after expiry while always preserving the primary secret (ADR-0021).
|
||||
type JWTSecretManager interface {
|
||||
AddSecret(secret string, isPrimary bool, expiresIn time.Duration)
|
||||
RotateToSecret(newSecret string)
|
||||
GetPrimarySecret() string
|
||||
GetAllValidSecrets() []JWTSecret
|
||||
GetSecretByIndex(index int) (string, bool)
|
||||
|
||||
// RemoveExpiredSecrets drops every non-primary secret whose ExpiresAt is
|
||||
// non-nil and in the past. Returns the count of secrets removed.
|
||||
// The primary secret is never removed regardless of expiration.
|
||||
RemoveExpiredSecrets() int
|
||||
|
||||
// StartCleanupLoop spawns a goroutine that calls RemoveExpiredSecrets at
|
||||
// the given interval. Stops when the context is cancelled. Safe to call
|
||||
// once at startup; calling again replaces the previous loop's context.
|
||||
StartCleanupLoop(ctx context.Context, interval time.Duration)
|
||||
}
|
||||
|
||||
// JWTService defines interface for JWT operations
|
||||
|
||||
Reference in New Issue
Block a user