Source:
ocean/docs/SECURITY_FALLBACK_ANALYSIS.md| ✏️ Edit on GitHub
Security Analysis: Fallback Risks in JWT Verification
Potential Attack Vectors
1. Downgrade Attacks
Risk: Attacker forces system to use weaker authentication method
- Force JWT verification to fail, triggering fallback to database lookup
- Exploit timing differences between local verification and API calls
- DoS the JWKS endpoint to force fallbacks
2. Token Confusion
Risk: Different validation rules between primary and fallback
- JWT verifier might have stricter rules than Supabase's getUser()
- Expired tokens might pass one check but not the other
- Role/permission mismatches between methods
3. Cache Poisoning
Risk: Attacking the JWKS cache
- Serve invalid keys to make all tokens fail verification
- Force system into fallback mode consistently
- Exploit cache TTL windows
4. Timing Attacks
Risk: Behavioral differences reveal information
- Failed JWT verification takes different time than API fallback
- Can enumerate valid vs invalid tokens
- Can detect when system is in fallback mode
Recommended Approach: No Fallbacks
// SECURE: Fail closed, no fallbacks
export class SecureAuthService {
async verifyToken(token: string): Promise<AuthResult> {
try {
const result = await jwtVerifier.verifyToken(token)
if (!result.valid) {
// FAIL CLOSED - No fallback
await this.logSecurityEvent('jwt_verification_failed', { token })
throw new AuthenticationError('Invalid token')
}
return { success: true, user: result.payload }
} catch (error) {
// FAIL CLOSED - No fallback
await this.logSecurityEvent('jwt_verification_error', { error })
throw new AuthenticationError('Authentication failed')
}
}
}
Security-First Design
1. Single Source of Truth
- JWT verification is THE authentication method
- No fallbacks, no alternate paths
- Consistent security posture
2. Fail Closed
- Any failure = access denied
- No degraded modes
- No "helpful" workarounds
3. Monitoring Required
interface SecurityEvent {
event: 'jwt_verification_failed' | 'jwks_fetch_failed' | 'token_expired'
timestamp: Date
metadata: Record<string, any>
}
class SecurityMonitor {
async logEvent(event: SecurityEvent) {
// Log to SIEM
// Alert on anomalies
// Track failure rates
}
}
Migration Strategy Without Fallbacks
Phase 1: Parallel Validation (Monitoring Only)
// Run both, but only use Supabase result
const jwtResult = await jwtVerifier.verifyToken(token)
const supabaseResult = await supabase.auth.getUser(token)
// Log discrepancies but don't act on them
if (jwtResult.valid !== !!supabaseResult.data.user) {
await logDiscrepancy({ jwtResult, supabaseResult })
}
// Still use Supabase for now
return supabaseResult
Phase 2: JWT Primary with Kill Switch
const FEATURE_FLAGS = {
useJwtVerification: true, // Can disable in emergency
}
if (FEATURE_FLAGS.useJwtVerification) {
return await jwtVerifier.verifyToken(token)
} else {
return await supabase.auth.getUser(token)
}
Phase 3: JWT Only
- Remove Supabase auth calls
- Remove feature flags
- Pure JWT verification
Key Principles
-
Never Mix Authentication Methods
- Choose one method and stick with it
- Mixing creates complexity and attack surface
-
Fail Closed Always
- No token = no access
- Invalid token = no access
- Verification error = no access
-
Monitor Everything
- Track all failures
- Alert on anomalies
- Have incident response ready
-
Test Security Scenarios
describe('Security Tests', () => {
it('should reject when JWKS is unavailable', async () => {
// Block JWKS endpoint
await expect(authService.verifyToken(validToken)).rejects.toThrow('Authentication failed')
})
it('should reject expired tokens', async () => {
await expect(authService.verifyToken(expiredToken)).rejects.toThrow('Authentication failed')
})
})
Recommendation
Remove all fallbacks from the enhanced auth service. The JWT verifier should be the sole authentication method. If it fails, access is denied. This is more secure than trying to be "helpful" with fallbacks.