diff --git a/backend/internal/services/emergency_token_service.go b/backend/internal/services/emergency_token_service.go index aeecfd89..2c61ed1c 100644 --- a/backend/internal/services/emergency_token_service.go +++ b/backend/internal/services/emergency_token_service.go @@ -147,34 +147,42 @@ func (s *EmergencyTokenService) Validate(token string) (*models.EmergencyToken, return nil, fmt.Errorf("token is empty") } + envToken := os.Getenv(EmergencyTokenEnvVar) + hasValidEnvToken := envToken != "" && len(strings.TrimSpace(envToken)) >= MinTokenLength + // Try database token first (highest priority) var tokenRecord models.EmergencyToken err := s.db.First(&tokenRecord).Error if err == nil { // Found database token - validate hash tokenHash := sha256.Sum256([]byte(token)) - if bcrypt.CompareHashAndPassword([]byte(tokenRecord.TokenHash), tokenHash[:]) != nil { - return nil, fmt.Errorf("invalid token") + if bcrypt.CompareHashAndPassword([]byte(tokenRecord.TokenHash), tokenHash[:]) == nil { + // Check expiration + if tokenRecord.IsExpired() { + return nil, fmt.Errorf("token expired") + } + + // Update last used timestamp and use count + now := time.Now() + tokenRecord.LastUsedAt = &now + tokenRecord.UseCount++ + if err := s.db.Save(&tokenRecord).Error; err != nil { + logger.Log().WithError(err).Warn("Failed to update token usage statistics") + } + + return &tokenRecord, nil } - // Check expiration - if tokenRecord.IsExpired() { - return nil, fmt.Errorf("token expired") + // If DB token doesn't match, allow explicit environment token as break-glass fallback. + if hasValidEnvToken && envToken == token { + logger.Log().Debug("Emergency token validated from environment variable while database token exists") + return nil, nil } - // Update last used timestamp and use count - now := time.Now() - tokenRecord.LastUsedAt = &now - tokenRecord.UseCount++ - if err := s.db.Save(&tokenRecord).Error; err != nil { - logger.Log().WithError(err).Warn("Failed to update token usage statistics") - } - - return &tokenRecord, nil + return nil, fmt.Errorf("invalid token") } // Fallback to environment variable for backward compatibility - envToken := os.Getenv(EmergencyTokenEnvVar) if envToken == "" || len(strings.TrimSpace(envToken)) == 0 { return nil, fmt.Errorf("no token configured") } diff --git a/backend/internal/services/emergency_token_service_test.go b/backend/internal/services/emergency_token_service_test.go index 8a302513..033593ad 100644 --- a/backend/internal/services/emergency_token_service_test.go +++ b/backend/internal/services/emergency_token_service_test.go @@ -222,7 +222,7 @@ func TestEmergencyTokenService_Validate_EnvironmentFallback(t *testing.T) { assert.Nil(t, tokenRecord, "Env var tokens return nil record") } -func TestEmergencyTokenService_Validate_DatabaseTakesPrecedence(t *testing.T) { +func TestEmergencyTokenService_Validate_EnvironmentBreakGlassFallback(t *testing.T) { db := setupEmergencyTokenTestDB(t) svc := NewEmergencyTokenService(db) @@ -239,9 +239,9 @@ func TestEmergencyTokenService_Validate_DatabaseTakesPrecedence(t *testing.T) { _, err = svc.Validate(dbResp.Token) assert.NoError(t, err) - // Environment token should NOT validate (database takes precedence) + // Environment token should still validate as break-glass fallback _, err = svc.Validate(envToken) - assert.Error(t, err) + assert.NoError(t, err) } func TestEmergencyTokenService_GetStatus(t *testing.T) {