# OAuth vulnerabilities: attacks, fixes and a testing checklist

Source: https://penetrationtestservices.com/vulnerabilities/oauth-vulnerabilities/
Publisher: Excession Labs (penetrationtestservices.com)
Last updated: 2026-11-09

OAuth vulnerabilities explained: redirect_uri, state, PKCE, implicit flow, mix-up and consent flaws, with fixes for four stacks, dated cases and a test checklist.

> **Short answer:** OAuth vulnerabilities are flaws in how an app or authorization server implements OAuth 2.0 or OpenID Connect, not in the protocol itself. The common ones are loose redirect_uri matching, a missing state check, no PKCE, implicit flow token leakage, mix-up between providers and unsafe account linking. RFC 9700 sets the fixes: exact redirect matching, PKCE, the code flow and audience-limited tokens.

OAuth vulnerabilities are flaws in how a client app or an authorization server implements OAuth 2.0 or OpenID Connect. The protocol lets a user give an app limited access to an account without sharing a password. The flaws come from the implementation: loose redirect checks, missing state, no PKCE, and account logic that trusts the wrong claim. The IETF's current guidance is RFC 9700 (BCP 240, January 2025).

This page maps eight attacks to their fixes, shows request examples against a fictional app, gives code for four stacks, lists dated incidents and ends with a test checklist.

## Where do OAuth flaws live?

OAuth has three parties: the user, the client app, and the authorization server that issues codes and tokens. A fourth, the resource server, accepts the access token. A flaw can sit in any hand-off between them.

In the authorization code flow, the client sends the browser to the authorization server with a client_id, a redirect_uri, a scope, a state value and (with PKCE) a code challenge. The user signs in. The server sends the browser back to the redirect_uri with a code. The client swaps the code for tokens on a back channel. Each parameter in that chain is a place where a tester asks: what if this is missing, changed or reused?

If the app logs users in with OAuth, it often receives an OpenID Connect ID token, which is a JWT. Signature and claim flaws then apply. See [JWT vulnerabilities](/vulnerabilities/jwt-vulnerabilities/).

| Technology | Job | Output |
|---|---|---|
| OAuth 2.0 | Delegated access to an API | Access token |
| OpenID Connect | Login on top of OAuth | ID token (a JWT) |
| SAML | Login with signed XML | Assertion |
| JWT | A token format, not a protocol | Signed claims |

## How do the main OAuth attacks work?

Eight patterns cover most findings. PortSwigger and the OWASP cheat sheet describe the same set.

| Attack | Flaw | What a tester checks | Fix (RFC 9700) |
|---|---|---|---|
| redirect_uri manipulation | Prefix, domain-only or wildcard matching | Does a changed redirect_uri still get a code? | Exact string match |
| Open redirect chain | An open redirect on a registered domain | Does a registered page forward to any URL? | Remove open redirectors |
| Missing or unbound state | Callback does not check state | Does the callback accept a code with no state, or another session's state? | Per-session random state or PKCE |
| Code interception | No PKCE; code readable by another app | Does the token endpoint work without a code_verifier? | Require PKCE (S256) |
| Implicit flow leakage | Token returned in the URL fragment | Is response_type=token enabled? | Disable; use code flow |
| Mix-up | Client works with several providers and trusts the wrong response | Is the issuer recorded and checked? | iss parameter (RFC 9207) |
| Scope and consent abuse | Broad scopes; users grant consent to malicious apps | Which scopes does the client ask for? | Least privilege; admin consent policy |
| Unsafe account linking | App matches users by a mutable email claim | Can a provider account with the same email take over a local one? | Key accounts on the sub claim |

### Redirect URI and open redirect chains

The redirect_uri decides where the code goes. If the server accepts any URL under a registered domain, or only checks the host, an attacker who can make a victim's browser carry a crafted link may route the code to a page they control. RFC 9700 says authorization servers must use exact string matching, except for ports in localhost redirects of native apps. It also says clients and servers must not expose open redirectors. See [open redirect](/vulnerabilities/open-redirect/) and CWE-601.

> **Illustrative redirect check: Harbor Notes:** This is a demonstration. No real customer system was tested. Harbor Notes is a fictional app and example.com is a reserved example domain.
> 
> The registered redirect is https://app.harbor-notes.example.com/callback. A tester, using a test account and a test client, sends a control first, then a variant:
> 
> ```http
> GET /authorize?response_type=code&client_id=harbor-web
>   &redirect_uri=https://app.harbor-notes.example.com/callback
>   &scope=notes.read&state=a1b2c3&code_challenge=&code_challenge_method=S256 HTTP/1.1
> Host: auth.harbor-notes.example.com
> ```
> 
> The control returns 302 to the registered callback. The variant changes the path to /callback/x. A strict server answers 400 with an error page and does not redirect. A vulnerable server issues a 302 with a code to the changed URL.
A lenient redirect is a weakness. It becomes account takeover only when the code can be delivered somewhere the tester controls and redeemed. Reports should state which of the two was proven.

### State, login CSRF and code interception

The state value ties the callback to the browser session that started the flow. If the callback does not check it, an attacker can start a flow with their own account, then get a victim's browser to finish it. The victim ends up signed in as the attacker, or the attacker's provider account gets linked to the victim's profile. This is login CSRF. See [CSRF testing](/guides/csrf-testing/).

PKCE (RFC 7636) stops a different attack: a stolen authorization code. The client sends a hashed challenge with the request and the original verifier with the token call. A thief who has only the code cannot redeem it. RFC 9700 says public clients must use PKCE, authorization servers must support it, and the server must reject a token request that carries a code_verifier when the original request had no code_challenge. That last rule blocks downgrade.

PKCE does not fix a lenient redirect_uri, a missing state check where no other binding exists, or a stolen access token. Each needs its own control.

### Implicit flow, mix-up and scope abuse

The implicit grant returns the access token in the redirect URL. RFC 9700 says clients should not use it, because tokens in URLs leak through history, logs and referrers and cannot be sender-constrained. Use the code flow with PKCE.

A mix-up attack targets a client that works with more than one authorization server. The attacker makes the client send a code meant for one server to another. RFC 9700 requires a defense when a client uses several servers. The preferred one is the iss response parameter from RFC 9207, which the client compares with the issuer it recorded for that request.

Scope abuse comes from over-broad grants. A client that asks for admin or write scopes it does not need makes every stolen token worth more. Consent phishing uses a real consent screen: the user approves a malicious app. Control it with admin approval for new apps and a review of granted apps.

### OpenID Connect extras

OpenID Connect adds checks of its own. Validate the ID token signature, iss, aud, exp and the nonce. PortSwigger's OpenID Connect page describes unprotected dynamic client registration, where an open registration endpoint lets anyone create a client, and request_uri parameters that make the server fetch a URL. Turn off dynamic registration unless you need it, and authenticate it when you do.

## Which real OAuth incidents have dated sources?

Two cases show how implementation and ecosystem flaws play out.

| Date | Case | What happened | Source |
|---|---|---|---|
| 12 to 27 April 2022 | Heroku and Travis CI OAuth tokens | GitHub investigated from 12 April. An attacker used stolen OAuth tokens issued to Heroku Dashboard apps (four) and a Travis CI app (one) to download private repositories from dozens of organizations, including npm. GitHub revoked tokens and notified users in stages, on 18, 22 and 27 April. | GitHub security alert |
| 11 April to 20 June 2023 | nOAuth | Descope reported to Microsoft on 11 April. Some apps used the mutable email claim, not the sub claim, to identify users with Microsoft login. An admin of their own tenant could set an email to a victim's address and sign in as them. Microsoft shipped fixes and a joint disclosure was published on 20 June. | Descope write-up |

Both cases are about trust placed in a token or a claim. The Heroku and Travis CI tokens worked wherever they were presented. The nOAuth apps trusted a claim that the user controlled. Descope's fix is to identify users by the sub claim and to verify email ownership before merging accounts.

## How do penetration testers detect and prove an OAuth flaw?

A finding needs a second session that ends up with access it should not have. Testers run controls beside every variant. This follows our method: write evidence is read back from another session, and a bare success response is not proof.

1. Map the flow: capture authorize, callback and token requests in a browser. Note response_type, redirect_uri, state, PKCE parameters and scopes.
2. Run controls: the exact registered redirect_uri must work, a changed one must fail, and a reused code must fail.
3. Test state: remove it, change it, and reuse it from a second browser. Browser-observed proof matters here, because state and cookies behave differently in a real browser than in a raw request.
4. Test PKCE: call the token endpoint with no code_verifier and with a wrong one.
5. Test linking: with two test accounts, change the email at the provider and check whether the app merges accounts on email.
6. Read back: confirm which user the final session belongs to and save the request and response with a timestamp.

> **Sample evidence note:** Demonstration only. Harbor Notes is fictional. Control: wrong code_verifier returns 400. Test: token request with no code_verifier returns 200 and an access token. Read-back: GET /api/v1/me with that token returns test user A. Result: PKCE is advertised but not enforced. Severity: Medium on the four-level scale, because exploitation also needs a way to read the code.
Testing needs written authorization from the system owner, test-owned accounts, and bounded volume against any paid or rate-limited provider.

## How do you fix OAuth in your code?

Use a maintained client library, enable PKCE and state, and register exact redirect URIs. The examples show the settings that matter.

```js
// Node.js, openid-client v5
const { Issuer, generators } = require("openid-client");
const issuer = await Issuer.discover("https://auth.example.com");
const client = new issuer.Client({
  client_id: "harbor-web",
  redirect_uris: ["https://app.example.com/callback"],
  response_types: ["code"],
});
const codeVerifier = generators.codeVerifier();
const url = client.authorizationUrl({
  scope: "openid notes.read",
  state: generators.state(),
  code_challenge: generators.codeChallenge(codeVerifier),
  code_challenge_method: "S256",
});
// Store state and codeVerifier in the server session; check both in the callback.
```

```python
# Python, Authlib (Flask)
oauth.register(
    name="harbor",
    server_metadata_url="https://auth.example.com/.well-known/openid-configuration",
    client_kwargs={"scope": "openid email", "code_challenge_method": "S256"},
)
# authorize_redirect() sets state; authorize_access_token() checks it.
```

```java
// Java, Spring Security: the code flow is the default for oauth2Login.
http.oauth2Login(Customizer.withDefaults());
// Register the exact redirect URI: {baseUrl}/login/oauth2/code/harbor
// Spring applies PKCE to public clients; confirm it in your provider settings.
```

```go
// Go, golang.org/x/oauth2
verifier := oauth2.GenerateVerifier()
state := randomString(32) // crypto/rand, stored in the session
url := conf.AuthCodeURL(state, oauth2.S256ChallengeOption(verifier))
// In the callback: compare state, then conf.Exchange(ctx, code, oauth2.VerifierOption(verifier))
```

On the server side, set these rules in the authorization server: exact redirect matching, PKCE required, implicit grant off, short-lived and audience-limited access tokens, refresh token rotation, and sender-constrained tokens (mutual TLS or DPoP) where RFC 9700 recommends them. The resource server must check the token's audience and scope on every request. Key local accounts on the sub claim and the issuer, never on email alone.

## OAuth testing checklist

- List every client and its registered redirect URIs. Look for wildcards, http URIs and test hosts.
- Check redirect_uri with exact, changed path, changed host and extra parameters. Look for open redirects on registered hosts.
- Remove, change and replay state. Test the callback in a second browser.
- Call the token endpoint without and with a wrong code_verifier. Reuse an authorization code.
- Check whether response_type=token or the password grant still works.
- With several providers, check issuer handling.
- Read the granted scopes and compare them with what the app needs.
- Validate ID token handling: signature, iss, aud, exp and nonce.
- Test account linking with a changed provider email.
- Check dynamic client registration and request_uri.
- List third-party apps authorized in your tenant and remove unused ones.

## When should you get a professional OAuth test?

Get one when you add social or enterprise login, publish an OAuth API to third parties, or let customers connect their own identity provider. A tester needs two or more accounts per role and a view of your registered clients. Our [web application penetration testing](/web-application-penetration-testing/) covers login flows, and the [authentication testing checklist](/guides/authentication-testing-checklist/) lists the related checks. Testing starts only after written authorization from the system owner.

## Questions

### Is OAuth secure?

The protocol is sound when you follow the current best practice, RFC 9700. Most real flaws come from the implementation: loose redirect URIs, a missing state check or unsafe account linking. A pentest checks your client, your authorization server settings and the account logic around them.

### What is the difference between OAuth and OpenID Connect?

OAuth 2.0 grants an app access to a resource. OpenID Connect adds an identity layer on top: an ID token that says who signed in. Use OAuth for delegated API access and OpenID Connect for login. SAML does a similar login job with XML.

### Does PKCE replace the state parameter?

They stop different attacks. PKCE binds the authorization code to the client that asked for it, so a stolen code is useless. State ties the response to the browser session that started the flow and stops login CSRF. Keep state even with PKCE: it costs nothing and covers clients that do not enforce PKCE.

### How do I test an OAuth login for account takeover?

Use two test accounts. Start a flow as account A, capture the code or token, and try to complete it as account B or from another browser. A lenient redirect alone is a weakness. A second session that ends up signed in as A is the proof.

### Is the implicit flow still allowed?

RFC 9700 says clients should not use the implicit grant unless token injection and leakage are mitigated. Use the authorization code flow with PKCE for browser apps and mobile apps.

## Sources

- [RFC 9700: Best Current Practice for OAuth 2.0 Security (BCP 240)](https://www.rfc-editor.org/rfc/rfc9700.html)
- [RFC 7636: Proof Key for Code Exchange (PKCE)](https://www.rfc-editor.org/rfc/rfc7636.html)
- [RFC 9207: OAuth 2.0 Authorization Server Issuer Identification](https://www.rfc-editor.org/rfc/rfc9207.html)
- [OWASP OAuth 2.0 Protocol Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/OAuth2_Cheat_Sheet.html)
- [PortSwigger Web Security Academy: OAuth 2.0 authentication vulnerabilities](https://portswigger.net/web-security/oauth)
- [PortSwigger Web Security Academy: OpenID Connect](https://portswigger.net/web-security/oauth/openid)
- [CWE-601: URL Redirection to Untrusted Site (Open Redirect)](https://cwe.mitre.org/data/definitions/601.html)
- [GitHub: Security alert, stolen OAuth user tokens (April 2022)](https://github.blog/news-insights/company-news/security-alert-stolen-oauth-user-tokens/)
- [Descope: nOAuth, how anyone can take over accounts in OAuth apps that use Microsoft login (June 2023)](https://www.descope.com/blog/post/noauth)
