Production hardening: security, resilience, observability, and compliance
Password complexity: custom validator requiring uppercase, lowercase, digit (min 8 chars)
Token expiry: 90-day token lifetime with refresh endpoint (60-90 day renewal window)
Health check: /api/health/ now pings Postgres + Redis, returns 503 on failure
Audit logging: async audit_log table for auth events (login, register, delete, etc.)
Circuit breaker: APNs/FCM push sends wrapped with 5-failure threshold, 30s recovery
FK indexes: 27 missing foreign key indexes across all tables (migration 017)
CSP header: default-src 'none'; frame-ancestors 'none'
Gzip compression: level 5 with media endpoint skipper
Prometheus metrics: /metrics endpoint using existing monitoring service
External timeouts: 15s push, 30s SMTP, context timeouts on all external calls
Migrations: 016 (token created_at), 017 (FK indexes), 018 (audit_log)
Tests: circuit breaker (15), audit service (8), token refresh (7), health (4),
middleware expiry (5), validator (new)
This commit is contained in:
@@ -188,6 +188,66 @@ func (s *AuthService) Register(req *requests.RegisterRequest) (*responses.Regist
|
||||
}, code, nil
|
||||
}
|
||||
|
||||
// RefreshToken handles token refresh logic.
|
||||
// - If token is expired (> expiryDays old), returns error (must re-login).
|
||||
// - If token is in the renewal window (> refreshDays old), generates a new token.
|
||||
// - If token is still fresh (< refreshDays old), returns the existing token (no-op).
|
||||
func (s *AuthService) RefreshToken(tokenKey string, userID uint) (*responses.RefreshTokenResponse, error) {
|
||||
expiryDays := s.cfg.Security.TokenExpiryDays
|
||||
if expiryDays <= 0 {
|
||||
expiryDays = 90
|
||||
}
|
||||
refreshDays := s.cfg.Security.TokenRefreshDays
|
||||
if refreshDays <= 0 {
|
||||
refreshDays = 60
|
||||
}
|
||||
|
||||
// Look up the token
|
||||
authToken, err := s.userRepo.FindTokenByKey(tokenKey)
|
||||
if err != nil {
|
||||
return nil, apperrors.Unauthorized("error.invalid_token")
|
||||
}
|
||||
|
||||
// Verify ownership
|
||||
if authToken.UserID != userID {
|
||||
return nil, apperrors.Unauthorized("error.invalid_token")
|
||||
}
|
||||
|
||||
tokenAge := time.Since(authToken.Created)
|
||||
expiryDuration := time.Duration(expiryDays) * 24 * time.Hour
|
||||
refreshDuration := time.Duration(refreshDays) * 24 * time.Hour
|
||||
|
||||
// Token is expired — must re-login
|
||||
if tokenAge > expiryDuration {
|
||||
return nil, apperrors.Unauthorized("error.token_expired")
|
||||
}
|
||||
|
||||
// Token is still fresh — no-op refresh
|
||||
if tokenAge < refreshDuration {
|
||||
return &responses.RefreshTokenResponse{
|
||||
Token: tokenKey,
|
||||
Message: "Token is still valid.",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Token is in the renewal window — generate a new one
|
||||
// Delete the old token
|
||||
if err := s.userRepo.DeleteToken(tokenKey); err != nil {
|
||||
log.Warn().Err(err).Str("token", tokenKey[:8]+"...").Msg("Failed to delete old token during refresh")
|
||||
}
|
||||
|
||||
// Create a new token
|
||||
newToken, err := s.userRepo.CreateToken(userID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
return &responses.RefreshTokenResponse{
|
||||
Token: newToken.Key,
|
||||
Message: "Token refreshed successfully.",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Logout invalidates a user's token
|
||||
func (s *AuthService) Logout(token string) error {
|
||||
return s.userRepo.DeleteToken(token)
|
||||
|
||||
Reference in New Issue
Block a user