Source:
ocean/docs/adr/0006-fail-closed-authentication.md| ✏️ Edit on GitHub
ADR-0006: Fail-Closed Authentication Without Fallbacks
Status
Accepted
Context
The user raised a critical security concern: "are fallbacks a vector for risk?" This prompted a comprehensive security analysis of our authentication system, which previously included fallback mechanisms that could potentially be exploited.
Security Risks Identified
- Downgrade Attacks: Attackers could force the system to use weaker authentication methods by making primary verification fail
- Token Confusion: Different validation rules between primary and fallback methods could be exploited
- Cache Poisoning: Attacking the JWKS cache to force consistent fallback mode
- Timing Attacks: Behavioral differences revealing information about valid vs invalid tokens
Decision
Implement a fail-closed authentication service with zero fallbacks. If JWT verification fails for any reason, access is denied. No alternate authentication paths, no degraded modes, no "helpful" workarounds.
Consequences
Positive
- Stronger Security Posture: No attack vectors through fallback mechanisms
- Consistent Behavior: Single authentication path reduces complexity
- Clear Failure Modes: Any failure = access denied, no ambiguity
- Audit Trail: Comprehensive security event logging for monitoring
Negative
- Less Resilient: If JWKS endpoint is down, all authentication fails
- No Graceful Degradation: Users locked out during any verification issues
- Stricter Requirements: All tokens must pass verification, no exceptions
Implementation
Core Principles
- Single Source of Truth: JWT verification is THE authentication method
- Fail Closed Always: Any error or invalid token = access denied
- Comprehensive Logging: All security events logged for monitoring
- No Fallbacks: Removed all alternate authentication paths
Code Structure
export class SecureAuthService {
async getSession(): Promise<{ session: Session | null; error?: Error }> {
// 1. Get session from Supabase
// 2. Verify JWT - if fails, clear session and deny access
// 3. Return session only if verification succeeds
}
async getUserFromToken(token: string): Promise<{ user: User | null; error?: Error }> {
// 1. Verify and extract user from JWT
// 2. Validate required fields (id, email)
// 3. Fail closed on any error
}
}
Security Event Types
jwt_verification_failed: Token verification failedjwt_verification_error: Error during verificationjwks_fetch_failed: Could not fetch public keyssession_fetch_failed: Could not get sessionuser_extraction_failed: Could not extract user from tokeninvalid_user_data: User data incompleterefresh_token_invalid: Refreshed token failed verificationauth_state_invalid_token: Auth state change with invalid token
Migration Path
- Deploy new secure auth service alongside existing
- Monitor security events to understand failure patterns
- Switch to secure auth service with feature flag
- Remove old auth code once stable
Alternatives Considered
1. Fallback to Database Lookup
Rejected: Creates downgrade attack vector where attacker forces JWT verification to fail
2. Cached User Sessions
Rejected: Cache poisoning could allow invalid sessions to persist
3. Emergency Override Tokens
Rejected: Backdoor that undermines entire security model
References
- OWASP Authentication Cheat Sheet
- JWT Best Practices
- Security Analysis:
/docs/SECURITY_FALLBACK_ANALYSIS.md