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:
2026-05-05 09:08:19 +02:00
parent 4afc15b82e
commit 405a9fc937
5 changed files with 73 additions and 10 deletions

View File

@@ -139,10 +139,16 @@ func initializeUserServices(cfg *config.Config) (user.UserRepository, user.UserS
return nil, nil, fmt.Errorf("failed to create PostgreSQL user repository: %w", err)
}
// Create JWT config
// Create JWT config.
// GetTTL is a method value — it captures cfg, so when WatchAndApply
// re-unmarshals into the same Config struct on file changes, every
// subsequent token generation reads the new TTL (ADR-0023 Phase 2).
// ExpirationTime is kept as a static fallback for tests that build
// JWTConfig manually without a Config.
jwtConfig := user.JWTConfig{
Secret: cfg.GetJWTSecret(),
ExpirationTime: time.Hour * 24, // 24 hours
ExpirationTime: 24 * time.Hour,
GetTTL: cfg.GetJWTTTL,
Issuer: "dance-lessons-coach",
}