Skip to content
Fast-turnaround security assessments available — 10+ years development & security experienceGet started
vulnerabilityCWE-863OWASP A01:2021Typical severity: High

OAuth 2.0 Security: Common Implementation Flaws and How to Exploit Them

·11 min read

OAuth 2.0 Security: Common Implementation Flaws and How to Exploit Them

OAuth 2.0 is not a protocol that is difficult to understand. The specification is readable, the flows are well-documented, and countless reference implementations exist. Yet OAuth deployments remain a consistent source of authorization vulnerabilities across web applications of every size.

The problem is not the protocol itself. It is the gap between reading the specification and implementing every security requirement it describes — especially the ones that appear optional until they are exploited.

What OAuth 2.0 Is Actually Doing

OAuth 2.0 is an authorization delegation framework. It allows a user to grant an application access to resources on a third-party service without sharing their credentials with that application. The user authenticates directly with the third-party service, which issues tokens that the application can use to access specific resources on the user's behalf.

The authorization code flow — the most secure and widely recommended variant — works in four steps:

  1. The client redirects the user to the authorization server with a request for specific scopes and a redirect URI where the response should be sent.
  2. The user authenticates and approves the access request. The authorization server redirects back to the client's redirect URI with a short-lived authorization code.
  3. The client exchanges the authorization code for an access token and optionally a refresh token, using its client credentials to authenticate to the token endpoint.
  4. The client uses the access token to make authorized requests to the resource server.

Each step in this flow has failure modes. Each failure mode has a security consequence.

Missing or Weak State Parameter Validation

The state parameter exists to prevent cross-site request forgery against the OAuth flow itself.

Before initiating an authorization request, the client generates a random, unguessable value, stores it in the user's session, and includes it as a query parameter in the authorization URL. When the authorization server redirects back to the client, it includes the same state value. The client reads the returned state, compares it to what it stored, and rejects the response if they do not match.

When this check is missing, the flow is vulnerable to CSRF. The attack works as follows:

  1. The attacker initiates an OAuth flow to link their own account on the target application to a legitimate external service.
  2. The attacker captures the authorization URL the target application generated but does not complete the flow — they stop before the redirect back.
  3. The attacker tricks a victim into clicking a link (or loading an image, or any mechanism that causes the browser to fetch a URL) that points to the OAuth callback URL with the attacker's authorization code and no state.
  4. The victim's browser completes the callback. If the application does not validate state, it processes the attacker's authorization code using the victim's session — linking the attacker's external account to the victim's application account.

The attacker can now log into the target application using their own external credentials and access the victim's account.

This attack requires no token theft, no session hijacking, and no man-in-the-middle position. It exploits the absence of a single comparison.

State parameter validation is listed as a security requirement in RFC 6749. It is frequently skipped because it is easy to overlook and has no visible effect on the normal flow.

Redirect URI Validation Failures

The authorization server is responsible for validating that the redirect_uri in each authorization request matches a URI registered by the legitimate client. If this validation is weak or absent, an attacker can redirect authorization codes to their own endpoint and exchange them for tokens.

Common Bypass Techniques

Suffix-only matching. Some servers validate that the redirect_uri ends with the registered domain, checking whether the URI contains the expected host as a suffix rather than an exact match. This check passes for https://evil.com?x=legitimate-app.com if the validation looks for legitimate-app.com as a substring.

Path prefix matching. If the server validates that the URI starts with the registered path, an attacker can append additional path components. A registered URI of https://app.example.com/callback passes a prefix check for https://app.example.com/callback/../../attacker.

Open redirects on the client domain. Even when redirect_uri is validated exactly, if the legitimate callback page contains an open redirect that can be exploited to forward the request, the authorization code can be redirected after it reaches the legitimate domain. The pattern is to redirect the user to the callback URL with a next parameter pointing to an attacker-controlled server, then exploit the open redirect to forward the code.

Wildcard subdomain matching. Some servers allow wildcard domains such as *.example.com. An attacker who can register or take over any subdomain of example.com — including via subdomain takeover of a defunct service — receives authorization codes for the entire domain space.

The correct implementation is exact string matching against a whitelist of pre-registered URIs with no wildcards, no suffix matching, and no path prefix matching. Any deviation from exact matching creates a bypass surface.

Authorization Code and Token Leakage

Referer Header Exposure

When the authorization server redirects to the callback URL with the authorization code as a query parameter, that URL appears in browser history and is sent as the Referer header when the callback page loads external resources.

If the callback page includes a third-party script, a tracking pixel, an embedded font, or any other cross-origin resource, the browser sends the full callback URL — including the authorization code — as the Referer header in each of those requests. The operators of those third-party services can read the authorization code from their access logs.

The standard mitigation is to use the Referrer-Policy: no-referrer header on the callback page and to exchange the authorization code for a token as quickly as possible. Short code lifetimes (under 10 minutes, often under 60 seconds) limit the window during which a leaked code is useful.

Server Access Log Exposure

Authorization codes in query parameters appear in server access logs. If the application logs the full request URI, authorization codes appear in log files that may be accessible to developers, operations teams, log aggregation services, or anyone with access to the logging infrastructure. Short code expiry times reduce this exposure but do not eliminate it.

Implicit Flow Fragment Exposure

The implicit flow, now deprecated but still present in legacy systems, returns access tokens directly as URL fragments in the redirect. Fragment values are not sent to servers in HTTP requests, but they are stored in browser history and accessible to JavaScript running on the callback page. Any cross-site scripting vulnerability on the callback page can extract the access token from window.location.hash.

The access token, unlike an authorization code, is a long-lived credential. There is no equivalent of server-side code expiry for access tokens that have already been issued. A leaked access token remains valid until it expires or is revoked.

PKCE: When It Protects and When It Fails

PKCE (Proof Key for Code Exchange) was introduced to protect OAuth flows in public clients — mobile applications and single-page applications that cannot securely store a client secret — against authorization code interception attacks.

The mechanism is straightforward: the client generates a random code_verifier, computes code_challenge = BASE64URL(SHA256(code_verifier)), and includes the challenge in the authorization request. When exchanging the code for a token, the client includes the original code_verifier. The server verifies that SHA256(code_verifier) matches the stored challenge. An attacker who intercepts the authorization code cannot exchange it without the verifier.

PKCE provides strong protection when both the client implements it correctly and the server enforces it. The failure modes are on the server side:

Server does not require PKCE. If the server accepts token exchange requests that omit the code_verifier for flows that were initiated with a code_challenge, PKCE provides no protection. An attacker who intercepts an authorization code simply omits the verifier parameter, and the server accepts the request. This is the most common PKCE implementation failure.

Server accepts plain method. PKCE supports two challenge methods: S256 (SHA-256 hash) and plain (the verifier is the challenge). With plain, the challenge and the verifier are identical. An attacker who can observe the authorization request — which is sent as a URL and appears in logs and Referer headers — sees the code_challenge and therefore has the code_verifier needed to exchange the code. Servers should reject plain method requests or not advertise it as supported.

PKCE applied to confidential clients without additional checks. Confidential clients can authenticate to the token endpoint using a client secret. Adding PKCE to a confidential client flow provides defense in depth but does not change the fundamental security model for code theft, since the client secret is already required.

Token Scope and Privilege Escalation

OAuth tokens carry scopes that define what the token is authorized to access. Scope downgrade and scope escalation vulnerabilities arise when:

The server does not validate that the issued token scope matches the requested scope. An authorization server that issues tokens with broader scopes than were requested allows clients — or attackers — to obtain access beyond what was granted. During testing, replacing the scope parameter value in authorization requests with broader scope values and verifying that the resulting token does not reflect those expanded scopes is a basic check.

Refresh token scope escalation. Some implementations allow a refresh token request to specify a different scope than the original grant. If the server does not validate that the requested scope is within the originally authorized set, an attacker who obtains a refresh token can escalate its privileges without the user's knowledge or re-consent.

Audience validation failure. Access tokens typically include an aud (audience) claim identifying which resource server should accept the token. Resource servers that do not validate the audience claim accept tokens intended for other services. In a system with multiple resource servers sharing the same authorization server, a token issued for a low-privilege service can be presented to a high-privilege service.

Identifying OAuth Vulnerabilities in Practice

When assessing an OAuth implementation, the systematic approach is to work through the authorization request, the callback handling, and the token exchange independently.

Authorization request: Test state parameter handling by replaying authorization callbacks with a state value that differs from the one stored in the session. A compliant implementation rejects the response. An omitted state check accepts it. Test redirect_uri validation by appending path components, adding query parameters, and trying URL-encoded variants to find where the validation breaks.

Callback handling: Check what external resources load on the callback page while the authorization code is present in the URL. Review whether the Referer policy prevents the code from being forwarded. Check whether the code is consumed in the callback handler before any redirects.

Token exchange: Test whether PKCE is enforced by omitting the code_verifier from exchange requests for PKCE-initiated flows. Test scope handling by requesting broader scopes and verifying the issued token does not reflect them.

Token storage and transmission: Verify tokens are not present in URL parameters in production usage. Check that tokens are transmitted only over TLS. Verify that refresh tokens are stored in secure, HttpOnly, SameSite=Strict cookies rather than in localStorage.

Remediation

The requirements for a secure OAuth implementation are well-specified:

  • Generate cryptographically random state values with at least 128 bits of entropy; validate them in every callback handler before processing any response parameters
  • Validate redirect_uri by exact string match against a pre-registered whitelist; do not accept wildcard entries or partial matches
  • Use the authorization code flow with PKCE for all public clients; enforce PKCE verification on the server and reject plain method
  • Set Referrer-Policy: no-referrer on all callback pages; expire authorization codes within 60 seconds of issuance; bind each code to the client that requested it
  • Validate token audience and scope on every protected resource endpoint; do not accept tokens whose aud claim does not match the current service
  • Store access tokens in memory; store refresh tokens in HttpOnly, SameSite=Strict cookies; never store credentials in localStorage or sessionStorage

The protocol is sound. Every common OAuth vulnerability is a failure to implement a requirement the specification states explicitly. The path from a vulnerable OAuth deployment to a secure one runs through the RFC, not around it.

For a practical example of OAuth-adjacent access control failures in production systems, see the access control case studies.

Need your application tested?

We find these vulnerabilities in real applications every day. Get a comprehensive security assessment with detailed remediation.

Request an Assessment

Summary

OAuth 2.0 is a widely implemented authorization framework, but its flexibility creates a large surface for implementation mistakes. Missing state parameter validation, unrestricted redirect URIs, leaking authorization codes through Referer headers, and improper token storage each represent exploitable pathways that bypass the security the protocol was designed to provide.

Key Takeaways

  • 1Missing or unvalidated state parameters in OAuth flows allow cross-site request forgery attacks that force users to authorize access under an attacker-controlled account
  • 2Overly permissive redirect_uri validation — including substring matching, path traversal, or wildcard domains — lets attackers redirect authorization codes to attacker-controlled endpoints
  • 3Authorization codes and access tokens appearing in browser history, server logs, and Referer headers expose them to theft long after the flow completes
  • 4PKCE was designed to protect public clients against authorization code interception, but only when the server enforces the code_verifier check and rejects requests missing it
  • 5Implicit flow access tokens returned directly in URL fragments are stored in browser history and can be leaked through postMessage misuse and fragment forwarding

Frequently Asked Questions

The state parameter is a random, unguessable value that the client generates before initiating an OAuth authorization request and includes as a query parameter. When the authorization server redirects back to the client with an authorization code, it includes the same state value. The client verifies that the returned state matches the one it sent. This binding prevents cross-site request forgery attacks in which a malicious site tricks a user's browser into completing an authorization flow that the attacker initiated — without state validation, an attacker can force a user to link their account to the attacker's OAuth token.

If the authorization server does not strictly validate the redirect_uri against a pre-registered exact URI, attackers can manipulate the parameter to redirect the authorization code to a domain they control. Common bypass techniques include adding extra path segments that a suffix-match check passes, using URL-encoded characters to confuse parsers, supplying a subdomain of the legitimate domain if the check only validates the domain suffix, or exploiting open redirects on the legitimate domain. Once the authorization code arrives at the attacker's endpoint, it can be exchanged for an access token using the application's client credentials.

PKCE (Proof Key for Code Exchange) adds a cryptographic binding between the authorization request and the token exchange. The client generates a random code_verifier, computes a code_challenge from it, and sends the challenge with the authorization request. When exchanging the authorization code for a token, the client provides the original code_verifier. The server verifies that the verifier matches the challenge it stored. PKCE fails to protect when the server does not enforce it — accepting token exchange requests without the code_verifier — or when it accepts the 'plain' method instead of 'S256', making the challenge trivially reversible.

When the authorization server redirects to a callback URL with an authorization code as a query parameter, and the callback page loads external resources — scripts, images, analytics — the browser sends the full callback URL as the Referer header in those subrequests. Any server receiving those requests can read the authorization code from the Referer header. Tokens returned in URL fragments are less exposed this way because fragments are not sent in Referer headers, but they appear in browser history and are accessible to JavaScript on the same page.

An OAuth mix-up attack occurs when a client interacts with multiple authorization servers and fails to verify that the tokens it receives came from the server it intended to use. An attacker who controls or compromises one authorization server in the set can redirect the client's authorization request to a different server. The client, not checking which server issued the token, uses credentials from the attacker's server to authenticate with the legitimate resource server, potentially leaking the legitimate server's tokens in the process. Mitigation requires the client to bind each authorization response to the specific issuer it targeted.