Introduction to OAuth 2.0 Security
OAuth 2.0 is the industry-standard protocol for authorization, enabling applications to obtain limited access to user accounts on third-party services. While OAuth 2.0 provides a robust framework for delegated authorization, improper implementations can introduce severe security vulnerabilities that attackers actively exploit.
Authorization Code Flow Best Practices
The authorization code flow is the most secure option for server-side applications. Two critical security parameters must be implemented correctly:
State Parameter Implementation
The state parameter prevents CSRF attacks during the OAuth flow:
import secrets
import time
def generate_oauth_state():
"""Generate a cryptographically secure state parameter."""
state = secrets.token_urlsafe(32)
session['oauth_state'] = state
session['oauth_state_created'] = time.time()
return state
def validate_oauth_state(received_state):
"""Validate the state parameter from callback."""
stored_state = session.pop('oauth_state', None)
state_created = session.pop('oauth_state_created', 0)
if not stored_state or not secrets.compare_digest(stored_state, received_state):
raise SecurityError("Invalid OAuth state parameter")
if time.time() - state_created > 300:
raise SecurityError("OAuth state parameter expired")
return TrueSecure Redirect URI Validation
Redirect URI validation is critical—open redirects enable token theft:
ALLOWED_REDIRECT_URIS = [
"https://app.example.com/oauth/callback",
"https://app.example.com/auth/complete"
]
def validate_redirect_uri(redirect_uri):
"""Strictly validate redirect URI against whitelist."""
if redirect_uri not in ALLOWED_REDIRECT_URIS:
raise SecurityError("Invalid redirect_uri")
parsed = urlparse(redirect_uri)
if parsed.scheme != "https":
raise SecurityError("redirect_uri must use HTTPS")
return TrueCommon vulnerable patterns to avoid:
# VULNERABLE: Substring matching allows bypass
if "example.com" in uri: # Attackers use: evil.com?x=example.com
return True
# VULNERABLE: Prefix matching allows subdomain attacks
if uri.startswith("https://example.com"):
return True # Attackers use: https://example.com.evil.comPKCE Implementation
Proof Key for Code Exchange (PKCE) protects the authorization code flow from interception attacks:
class PKCEClient {
async generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return this.base64UrlEncode(array);
}
async generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const digest = await crypto.subtle.digest('SHA-256', data);
return this.base64UrlEncode(new Uint8Array(digest));
}
base64UrlEncode(buffer) {
return btoa(String.fromCharCode(...buffer))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
async startAuthFlow() {
const codeVerifier = await this.generateCodeVerifier();
const codeChallenge = await this.generateCodeChallenge(codeVerifier);
sessionStorage.setItem('pkce_verifier', codeVerifier);
const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
// ... other params
window.location.href = authUrl.toString();
}
}Common OAuth Vulnerabilities
Authorization Code Injection
Bind authorization codes to the client session to prevent theft:
def create_auth_code(client_id, user_id, session_id):
code = secrets.token_urlsafe(32)
store_auth_code(code, {
'client_id': client_id,
'user_id': user_id,
'session_binding': hashlib.sha256(session_id.encode()).hexdigest(),
'created_at': time.time(),
'used': False
})
return code
def validate_auth_code(code, client_id, session_id):
data = get_auth_code(code)
if not data or data['used']:
return None
mark_code_used(code)
if data['client_id'] != client_id:
return None
if data['session_binding'] != hashlib.sha256(session_id.encode()).hexdigest():
return None
if time.time() - data['created_at'] > 600:
return None
return dataInsecure Token Storage
// VULNERABLE: localStorage accessible to XSS
localStorage.setItem('access_token', token);
// BETTER: Use in-memory storage with httpOnly refresh tokens
class TokenManager {
#accessToken = null;
setAccessToken(token) {
this.#accessToken = token;
}
async refreshToken() {
const response = await fetch('/api/refresh', {
method: 'POST',
credentials: 'include'
});
const data = await response.json();
this.setAccessToken(data.access_token);
}
}Real-World Attack Scenario
1. Attacker discovers weak redirect validation
2. Crafts malicious URL:
https://oauth.provider.com/authorize?
client_id=VICTIM_APP_ID&
redirect_uri=https://victim-app.com.evil.com/callback
3. If victim app uses substring matching, authorization code goes to evil.com
4. Attacker exchanges code for tokens and accesses victim's account
Security Checklist
- Use Authorization Code flow with PKCE for all clients
- Implement cryptographically random
stateparameter - Validate redirect URIs with exact string matching
- Use HTTPS for all OAuth endpoints
- Ensure authorization codes are single-use with short expiration
- Store tokens securely (httpOnly cookies or secure in-memory storage)
- Validate and restrict OAuth scopes
Automated OAuth Security Testing
OAuth implementations contain many moving parts, and misconfigurations are common. Manual review of authorization flows, redirect URI handling, and token management requires significant expertise.
RedVeil's AI-powered penetration testing platform can autonomously test your OAuth implementation for vulnerabilities including redirect URI bypasses, state parameter weaknesses, PKCE implementation flaws, and token leakage vectors. The platform validates findings through controlled exploitation and provides proof-of-concept evidence.