✨ merge: implement JWT secret rotation with BDD scenario isolation - Implement JWT secret rotation mechanism (closes #8) - Add per-scenario state isolation for BDD tests (closes #14) - Validate password reset workflow via BDD tests (closes #7) - Fix port conflicts in test validation - Add state tracer for debugging test execution - Document BDD isolation strategies in ADR 0025 - Fix PostgreSQL configuration environment variables Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai> Co-authored-by: Gabriel Radureau <arcodange@gmail.com> Co-committed-by: Gabriel Radureau <arcodange@gmail.com>
109 lines
2.6 KiB
Go
109 lines
2.6 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
|
|
}
|
|
|
|
// Reset resets the secret manager to its initial state with only the primary secret
|
|
// This is useful for test cleanup to ensure tests don't interfere with each other
|
|
func (m *JWTSecretManager) Reset(initialSecret string) {
|
|
m.secrets = []JWTSecret{
|
|
{
|
|
Secret: initialSecret,
|
|
IsPrimary: true,
|
|
CreatedAt: time.Now(),
|
|
},
|
|
}
|
|
m.primarySecret = initialSecret
|
|
}
|