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:

  1. 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 random state value
  2. The user authenticates at the authorization server and grants consent
  3. The authorization server redirects back to redirect_uri with an authorization code
  4. The client exchanges the authorization code for access tokens and refresh tokens at the token endpoint, using the client_secret
  5. 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:

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:

  1. Attacker authenticates with the authorization server and obtains an authorization code
  2. Attacker does NOT complete the exchange — instead captures the redirect URL containing the code
  3. Attacker tricks the victim into visiting that redirect URL (via an img tag, form POST, or link)
  4. Victim's browser sends the attacker's authorization code to the victim's application
  5. Application exchanges the code, linking the attacker's OAuth account to the victim's application session
  6. Attacker logs in — now accessing the victim's account

What pentesters test:


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

  1. Client generates a random high-entropy string: the code_verifier
  2. Client creates a code_challenge by hashing the verifier: SHA256(code_verifier), base64url-encoded
  3. Client includes code_challenge and code_challenge_method=S256 in the authorization request
  4. On token exchange, client includes the original code_verifier
  5. 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:

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


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:

  1. Attacker creates an account at a third-party OAuth provider (GitHub, Google) using the victim's email address
  2. Attacker initiates "Sign in with [Provider]" on the target application
  3. Application receives email victim@company.com from the OAuth provider
  4. Application links this to the existing account with that email
  5. 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:

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
← Back to Blog