| name | oauth-security |
| description | Secure OAuth 2.0 implementations against common attacks. Outputs threat model, security controls, token security hardening, and implementation review checklist. |
| argument-hint | ["OAuth flow","client type","token types","identified threats"] |
| allowed-tools | Read, Write |
OAuth Security
OAuth 2.0 has many implementation pitfalls. The spec is flexible; implementors fill gaps incorrectly. Common vulnerabilities include CSRF on the callback, open redirect abuse, token leakage via referrer headers, and insufficient token validation. Security requires implementing the spec correctly, not just partially.
OAuth Threat Model
## Common OAuth Attack Vectors
### 1. CSRF on Redirect URI (State Parameter Missing)
Attack: Attacker crafts authorization URL; tricks victim into authorising;
steals the resulting code via forged callback.
Mitigation: State parameter — cryptographically random, verified on callback.
### 2. Open Redirect via redirect_uri
Attack: Attacker registers redirect_uri with slight variation;
authorization code sent to attacker's server.
Mitigation: Exact match on registered redirect_uri — no wildcards.
### 3. Token Leakage via Referrer Header
Attack: Access token in URL fragment leaks in Referer header to third-party pages.
Mitigation: Tokens never in URL; use Authorization header only.
### 4. Authorization Code Interception (PKCE Bypass)
Attack: In public clients, authorization code intercepted and exchanged.
Mitigation: PKCE (Proof Key for Code Exchange) — S256 method.
### 5. JWT Algorithm Confusion
Attack: Server accepts "alg: none" or RS256 key used with HS256 algorithm.
Mitigation: Allowlist algorithms; validate strictly.
### 6. Token Replay
Attack: Stolen access or refresh token replayed by attacker.
Mitigation: Short token lifetimes; refresh token rotation; IP binding.
Security Controls Implementation
import secrets
import hashlib
import base64
from datetime import datetime, timedelta
import jwt
from jwt import PyJWKClient
def generate_state() -> str:
return secrets.token_urlsafe(32)
def verify_state(received: str, expected: str) -> bool:
return secrets.compare_digest(received, expected)
def generate_pkce() -> tuple[str, str]:
verifier = secrets.token_urlsafe(64)
challenge = base64.urlsafe_b64encode(
hashlib.sha256(verifier.encode()).digest()
).decode().rstrip("=")
return verifier, challenge
REGISTERED_URIS = {
"client_abc": {"https://app.example.com/callback"},
"mobile_app": {"myapp://auth/callback"},
}
def validate_redirect_uri(client_id: str, redirect_uri: str) -> bool:
allowed = REGISTERED_URIS.get(client_id, set())
return redirect_uri allowed
JWKS_CLIENT = PyJWKClient()
ALLOWED_ALGORITHMS = [, ]
() -> :
header = jwt.get_unverified_header(token)
header.get() ALLOWED_ALGORITHMS:
ValueError()
signing_key = JWKS_CLIENT.get_signing_key_from_jwt(token)
jwt.decode(
token,
signing_key.key,
algorithms=ALLOWED_ALGORITHMS,
audience=expected_audience,
options={
: [, , , , ],
: ,
: ,
}
)
() -> :
stored = token_store.get_refresh_token(old_token)
stored:
token_store.revoke_all_user_tokens(stored.user_id)
SecurityException()
stored.client_id != client_id:
ValueError()
token_store.revoke(old_token)
new_access = generate_access_token(stored.user_id)
new_refresh = secrets.token_urlsafe()
token_store.store_refresh_token(new_refresh, stored.user_id, client_id)
{: new_access, : new_refresh}
Token Security Hardening
ACCESS_TOKEN_TTL = timedelta(minutes=15)
REFRESH_TOKEN_TTL = timedelta(days=30)
def generate_access_token(user_id: str, scope: list[str]) -> str:
now = datetime.utcnow()
payload = {
"sub": user_id,
"iat": now,
"exp": now + ACCESS_TOKEN_TTL,
"jti": secrets.token_urlsafe(16),
"scope": " ".join(scope),
"iss": "https://auth.example.com",
"aud": "https://api.example.com",
}
return jwt.encode(payload, PRIVATE_KEY, algorithm="RS256")
Implementation Review Checklist
## OAuth Security Review Checklist
### Authorization Server
- [ ] State parameter generated and verified on callback
- [ ] redirect_uri exact match only — no wildcards
- [ ] PKCE required for public clients (S256)
- [ ] Authorization codes single-use and short-lived (<10 min)
- [ ] Client secrets hashed in storage
### Token Security
- [ ] Access token TTL ≤ 15 minutes
- [ ] Refresh token rotation on every use
- [ ] JWT "alg: none" rejected
- [ ] JWT algorithm allowlist (not ["*"])
- [ ] Both "aud" and "iss" claims validated
- [ ] Token revocation endpoint implemented
### Transport
- [ ] HTTPS only — no HTTP fallback
- [ ] Tokens in Authorization header — never in URL query parameters
- [ ] No tokens in server logs
Anti-Patterns to Avoid
| Anti-Pattern | Problem | Fix |
|---|
| Missing state parameter | CSRF on callback | Generate state; verify on callback |
| Wildcard redirect_uri | Code sent to attacker-controlled URI | Exact match only |
| JWT alg:none accepted | Signature bypass | Allowlist algorithms; reject none |
| Long access token TTL | Leaked token valid for hours | 15 minutes maximum |
| No refresh token rotation | Stolen refresh token valid indefinitely | Rotate on every use; detect reuse |
| Tokens in URL | Leaks in browser history, logs, referrer | Authorization header only |
10 Rules
- State parameter is mandatory — CSRF is real and exploitable without it.
- redirect_uri is exact match — never startsWith or pattern match.
- PKCE is required for all public clients — implicit flow is deprecated.
- JWT algorithm is explicitly allowlisted — never trust the "alg" header blindly.
- Validate both "aud" and "iss" — missing either enables cross-service token use.
- Access token TTL is 15 minutes — short enough that a stolen token expires quickly.
- Refresh token rotation detects theft — reuse attack triggers full session revocation.
- Tokens never appear in URLs — Authorization header only; no query parameters.
- Authorization codes are single-use — reject replays immediately.
- Revocation endpoint is implemented and tested — logout means logout, including tokens.