OAuth 2.0 Security Testing: Authorization Code, Token Attacks, and PKCE Flaws
OAuth 2.0 is the authorization framework behind nearly every "Sign in with Google / GitHub / Slack" button on the web. It's also one of the most consistently misimplemented protocols in security assessments — not because the spec is wrong, but because OAuth's flexibility creates dozens of implementation decisions, and each one is an opportunity for a security-relevant mistake.
When pentesters find OAuth on an engagement, they follow a systematic checklist that most development teams have never seen. This guide covers that checklist — what gets tested, why specific attacks work, and what remediation looks like.
OAuth 2.0 Flow Primer
The Authorization Code flow is the most common and the most security-relevant. The basic sequence:
- The client (your application) redirects the user to the authorization server (Google, GitHub, your IdP) with a request that includes
client_id,redirect_uri,scope,response_type=code, and a randomstatevalue - The user authenticates at the authorization server and grants consent
- The authorization server redirects back to
redirect_uriwith an authorization code - The client exchanges the authorization code for access tokens and refresh tokens at the token endpoint, using the
client_secret - The client uses the access token to call APIs on behalf of the user
Each step in this flow has security properties that can be violated. Pentesters test each one.
Step 1: Redirect URI Validation
The redirect URI is where the authorization code lands after the user authenticates. If the authorization server doesn't strictly validate this value, an attacker can redirect the authorization code to their own server.
Open redirect via partial matching
Weak redirect URI validation allows variations. If the registered URI is https://app.company.com/callback and the server uses prefix matching instead of exact matching:
https://auth.provider.com/oauth/authorize?
client_id=abc123&
redirect_uri=https://app.company.com.attacker.com/steal&
response_type=code&
scope=openid email
If this redirect is accepted, the authorization code is delivered to attacker.com. The attacker exchanges it before the legitimate client can, gaining access to the user's account.
Variations to test:
- Subdomain matching:
https://app.company.com.attacker.com/ - Path traversal:
https://app.company.com/../../../attacker.com - Fragment-based:
https://app.company.com/callback#@attacker.com/steal - URL encoding bypass:
https://app.company.com%2F@attacker.com/ - Wildcard abuse: if wildcards are permitted, e.g.,
https://*.company.com - HTTP vs HTTPS: if the server accepts
http://for a registeredhttps://redirect
Open redirect chaining
Even if the redirect URI is strictly validated, if the destination URL contains an open redirect vulnerability, the authorization code can still be stolen:
https://app.company.com/redirect?to=https://attacker.com/steal
If app.company.com/redirect is an open redirect registered as an allowed redirect URI, the code flows to attacker.com.
Step 2: State Parameter and CSRF
The state parameter in the authorization request serves as a CSRF token. It binds the authorization response to the specific user session that initiated the request.
Missing or predictable state
If the state parameter is absent or predictable, a CSRF attack works:
- Attacker authenticates with the authorization server and obtains an authorization code
- Attacker does NOT complete the exchange — instead captures the redirect URL containing the code
- Attacker tricks the victim into visiting that redirect URL (via an img tag, form POST, or link)
- Victim's browser sends the attacker's authorization code to the victim's application
- Application exchanges the code, linking the attacker's OAuth account to the victim's application session
- Attacker logs in — now accessing the victim's account
What pentesters test:
- Is the state parameter included in the authorization request?
- Is it a cryptographically random value (not predictable, not sequential)?
- Is the state value validated when the callback is received — does the application reject responses with a mismatched state?
- Is the state parameter tied to the user's session, not just stored globally?
Step 3: Authorization Code Security
Code reuse
Authorization codes should be single-use. If a code can be exchanged multiple times, an attacker who intercepts one exchange (via logs, referrer headers, or network eavesdropping) can use it again later.
Test: Exchange a valid authorization code at the token endpoint. Exchange the same code a second time. The second exchange should fail with an invalid_grant error. If it returns tokens again, codes are reusable.
Code expiration
Authorization codes should expire quickly — typically within 10 minutes per RFC 6749. If a code is valid for hours or days, the window for interception and replay attacks grows significantly.
Test: Obtain an authorization code, wait beyond the claimed expiration period, then attempt to exchange it. It should fail.
Authorization code injection
If the application doesn't validate that the authorization code was issued to the same client_id and redirect_uri used in the exchange, codes from other clients may be accepted. This is rare in well-implemented authorization servers but appears in custom implementations.
Step 4: PKCE (Proof Key for Code Exchange)
PKCE was introduced in RFC 7636 to protect public clients (single-page apps, mobile apps) that cannot securely store a client_secret. It's now recommended for all clients, including confidential ones.
How PKCE works
- Client generates a random high-entropy string: the
code_verifier - Client creates a
code_challengeby hashing the verifier:SHA256(code_verifier), base64url-encoded - Client includes
code_challengeandcode_challenge_method=S256in the authorization request - On token exchange, client includes the original
code_verifier - Authorization server verifies:
SHA256(received_verifier) == stored_code_challenge
An attacker who intercepts the authorization code cannot exchange it — they don't have the code verifier.
PKCE implementation flaws
Security testing looks for these PKCE failures:
- PKCE not required: The authorization server accepts authorization requests without a code challenge. PKCE is optional when it should be mandatory.
- Downgrade to plain method: The server accepts
code_challenge_method=plain, where the challenge equals the verifier. This provides no security — an attacker who intercepts the redirect also gets the challenge, which equals the verifier. - Challenge not validated: The server accepts any
code_verifierwithout verifying it matches the stored challenge. PKCE is present in the protocol but not enforced. - Short verifiers: The code verifier should be 43-128 characters. Very short verifiers are brute-forceable.
Test for PKCE bypass:
# Step 1: Start an authorization request WITH a code challenge
GET /oauth/authorize?
client_id=abc123&
redirect_uri=https://app.example.com/callback&
response_type=code&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256
# Step 2: Receive the authorization code
# code=SplxlOBeZQQYbYS6WxSbIA
# Step 3: Exchange WITHOUT the code_verifier
POST /oauth/token
grant_type=authorization_code&
code=SplxlOBeZQQYbYS6WxSbIA&
client_id=abc123&
redirect_uri=https://app.example.com/callback
# NO code_verifier included
# If this returns tokens, PKCE is not enforced
Step 5: Token Security
Access token leakage via Referer header
If an access token appears in a URL (as a query parameter), it's included in the Referer header when the user follows any link to an external site. Check all OAuth flows where tokens appear in URLs — the implicit flow (response_type=token) is particularly vulnerable to this.
The implicit flow is deprecated in OAuth 2.1 specifically because of token leakage risks. Test whether the application is still using it.
Token scope validation
OAuth scopes should restrict what an access token can do. A token issued for scope=read:profile should not be able to write data or access billing information.
Test: Obtain a low-scope token. Attempt to call endpoints that require higher scopes. The API should return 403 Insufficient Scope, not success.
Token binding and audience validation
If the authorization server issues JWTs, check whether the aud (audience) claim is validated by resource servers. A token issued for Resource Server A should not be accepted by Resource Server B — but often is if audience validation is skipped.
# JWT payload example
{
"sub": "user_123",
"iss": "https://auth.company.com",
"aud": "api-service-a", # should NOT be accepted by api-service-b
"scope": "read:orders",
"exp": 1751000000
}
Refresh token rotation and revocation
- Does refresh token rotation work? After exchanging a refresh token for a new access token, the old refresh token should be invalid
- Does refresh token revocation work? After logout, are refresh tokens invalidated server-side?
- Are refresh tokens tied to the client? A refresh token stolen from one client and used by another should be rejected
Step 6: Open Redirect at the Application Level
After the OAuth callback is processed, applications often redirect the user to their intended destination using a parameter:
https://app.company.com/oauth/callback?code=xxx&state=yyy&next=/dashboard
If the next parameter is used for redirection without validation:
https://app.company.com/oauth/callback?code=xxx&state=yyy&next=https://phishing.com
The user completes authentication, trusts the domain, and is redirected to a phishing page. Even without credential theft, this damages trust and can be used for further social engineering.
Step 7: OAuth for Authentication vs Authorization
OAuth 2.0 is an authorization framework, not an authentication protocol. Using raw OAuth access tokens to authenticate users (rather than OpenID Connect) creates vulnerabilities:
Account takeover via email claim trust
If the application uses OAuth to retrieve user identity by calling /userinfo and trusts the email address returned:
- Attacker creates an account at a third-party OAuth provider (GitHub, Google) using the victim's email address
- Attacker initiates "Sign in with [Provider]" on the target application
- Application receives email
victim@company.comfrom the OAuth provider - Application links this to the existing account with that email
- Attacker has taken over the victim's account
This is only exploitable when the third-party provider doesn't verify email ownership (which some do and some don't), and when the application doesn't require prior account linking. But the number of real-world account takeovers via this vector makes it worth testing.
OAuth Security Testing Checklist
| Test | What to Check |
|---|---|
| Redirect URI validation | Partial matching, subdomain manipulation, URL encoding, path traversal |
| State parameter | Present, random, validated on callback, session-bound |
| Authorization code | Single-use, short-lived (<10 min), bound to correct client |
| PKCE | Required (not optional), S256 method only, challenge validated |
| Token scope | Low-scope tokens cannot access high-scope endpoints |
| Token audience | Tokens not accepted across different resource servers |
| Refresh tokens | Rotated on use, revoked on logout, bound to issuing client |
| Implicit flow | Not used (deprecated in OAuth 2.1) |
| Post-auth redirect | No open redirect on the next or return_to parameter |
| Email claim trust | Account linking requires explicit consent, not automatic on email match |
Testing OAuth as Part of Your Security Program
OAuth testing requires manual review — automated scanners can identify some issues (open redirects, missing state parameters in some configurations) but cannot verify the full authorization code flow without active participation in the protocol.
For practical coverage:
- During development: Use an OAuth security library rather than hand-rolling the flow. Libraries from major identity providers implement these checks correctly. Custom implementations fail all the time.
- During security review: Map the complete OAuth flow, identify every parameter in every request, and test each parameter independently.
- During automated scanning: Automated tools can detect open redirect indicators in OAuth callback URLs, missing security headers on authorization pages, and deprecated grant types (implicit flow).
Ironimo scans web applications for OAuth-related misconfigurations, open redirect indicators, and missing security headers that affect authentication flows. Combined with manual review of the authorization flow itself, you get both automated coverage and the depth a one-off manual test provides.
Start free scan