✨ feat(auth): JWT TTL hot-reload + fix hardcoded 24h bug (ADR-0023 Phase 2)
Two changes in one diff because they share the same surface (JWTConfig plumbing): 1. **Bug fix** : pkg/server/server.go was hardcoding ExpirationTime to 24h, ignoring the auth.jwt.ttl config value entirely (default 1h). Production has been signing tokens with 24h TTL regardless of config since the config field was added. 2. **Hot-reload (ADR-0023 Phase 2)** : extends JWTConfig with a GetTTL func() time.Duration callback. effectiveTTL() prefers GetTTL when set, falls back to ExpirationTime otherwise (test-friendly). server.go wires GetTTL = cfg.GetJWTTTL — a method value that captures the *Config, so when WatchAndApply re-unmarshals, the next token generation reads the new TTL automatically. Tokens already issued keep their original expiry. WatchAndApply now also logs the new jwt_ttl on every reload event. Tests: - New TestWatchAndApply_JWTTTL in pkg/config/config_hot_reload_test.go rewrites the config file and asserts the in-memory ttl flips within 2s. Polling (no fixed sleep), race-clean. - Existing pkg/user tests (including JWT manager + cleanup loop) all pass with -race. - Full BDD suite (auth/config/greet/health/info/jwt) green. ADR-0023 status: Phase 1+2 Implemented. Phase 3 (telemetry sampler) and Phase 4 (api.v2_enabled — needs router refactor) remain Proposed.
This commit is contained in:
@@ -11,13 +11,30 @@ import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// JWTConfig holds JWT configuration
|
||||
// JWTConfig holds JWT configuration.
|
||||
//
|
||||
// GetTTL, when non-nil, is called on every token generation to read the
|
||||
// current TTL — this enables ADR-0023 Phase 2 hot-reload of `auth.jwt.ttl`.
|
||||
// If nil, ExpirationTime is used as a static fallback.
|
||||
type JWTConfig struct {
|
||||
Secret string
|
||||
ExpirationTime time.Duration
|
||||
GetTTL func() time.Duration
|
||||
Issuer string
|
||||
}
|
||||
|
||||
// effectiveTTL returns the live TTL: GetTTL() when wired, else
|
||||
// ExpirationTime as a static fallback (used by tests that don't go
|
||||
// through the server-level wiring).
|
||||
func (c JWTConfig) effectiveTTL() time.Duration {
|
||||
if c.GetTTL != nil {
|
||||
if ttl := c.GetTTL(); ttl > 0 {
|
||||
return ttl
|
||||
}
|
||||
}
|
||||
return c.ExpirationTime
|
||||
}
|
||||
|
||||
// userServiceImpl implements the unified UserService interface
|
||||
type userServiceImpl struct {
|
||||
repo UserRepository
|
||||
@@ -69,7 +86,7 @@ func (s *userServiceImpl) GenerateJWT(ctx context.Context, user *User) (string,
|
||||
"sub": user.ID,
|
||||
"name": user.Username,
|
||||
"admin": user.IsAdmin,
|
||||
"exp": time.Now().Add(s.jwtConfig.ExpirationTime).Unix(),
|
||||
"exp": time.Now().Add(s.jwtConfig.effectiveTTL()).Unix(),
|
||||
"iat": time.Now().Unix(),
|
||||
"iss": s.jwtConfig.Issuer,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user