Files
dance-lessons-coach/pkg/user/jwt_manager.go
Gabriel Radureau 07f8bd65b7
All checks were successful
CI/CD Pipeline / Build Docker Cache (push) Successful in 11s
CI/CD Pipeline / CI Pipeline (push) Successful in 4m32s
🧪 test: implement JWT secret rotation BDD tests
- Fix admin handler to handle flexible boolean parsing

- Modify GenerateJWT to use latest secret for signing

- Update JWT secret manager for proper expiration handling

- Fix BDD test steps to use actual tokens instead of hardcoded ones

- Add comprehensive debug logging for JWT operations

Resolves JWT secret rotation feature implementation

Generated by Mistral Vibe.

Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-04-09 16:14:31 +02:00

96 lines
2.2 KiB
Go

package user
import (
"time"
)
// JWTSecret represents a JWT secret with metadata
type JWTSecret struct {
Secret string
IsPrimary bool
CreatedAt time.Time
ExpiresAt *time.Time // Optional expiration time
}
// JWTSecretManager manages multiple JWT secrets for rotation
type JWTSecretManager struct {
secrets []JWTSecret
primarySecret string
}
// NewJWTSecretManager creates a new JWT secret manager
func NewJWTSecretManager(initialSecret string) *JWTSecretManager {
return &JWTSecretManager{
secrets: []JWTSecret{
{
Secret: initialSecret,
IsPrimary: true,
CreatedAt: time.Now(),
},
},
primarySecret: initialSecret,
}
}
// AddSecret adds a new JWT secret
func (m *JWTSecretManager) AddSecret(secret string, isPrimary bool, expiresIn time.Duration) {
var expiresAt *time.Time
if expiresIn > 0 {
expirationTime := time.Now().Add(expiresIn)
expiresAt = &expirationTime
}
// If expiresIn is 0 or negative, expiresAt remains nil (no expiration)
m.secrets = append(m.secrets, JWTSecret{
Secret: secret,
IsPrimary: isPrimary,
CreatedAt: time.Now(),
ExpiresAt: expiresAt,
})
if isPrimary {
m.primarySecret = secret
}
}
// RotateToSecret rotates to a new primary secret
func (m *JWTSecretManager) RotateToSecret(newSecret string) {
// Mark existing primary as non-primary
for i, secret := range m.secrets {
if secret.IsPrimary {
m.secrets[i].IsPrimary = false
break
}
}
// Add new secret as primary
m.AddSecret(newSecret, true, 0) // No expiration for primary
}
// GetPrimarySecret returns the current primary secret
func (m *JWTSecretManager) GetPrimarySecret() string {
return m.primarySecret
}
// GetAllValidSecrets returns all valid (non-expired) secrets
func (m *JWTSecretManager) GetAllValidSecrets() []JWTSecret {
var validSecrets []JWTSecret
now := time.Now()
for _, secret := range m.secrets {
if secret.ExpiresAt == nil || secret.ExpiresAt.After(now) {
validSecrets = append(validSecrets, secret)
}
}
return validSecrets
}
// GetSecretByIndex returns a secret by index for testing
func (m *JWTSecretManager) GetSecretByIndex(index int) (string, bool) {
if index < 0 || index >= len(m.secrets) {
return "", false
}
return m.secrets[index].Secret, true
}