API Security

OAuth 2.0 & OIDC: Misconfigurations and Attack Vectors

The overwhelming majority of vulnerabilities found in OAuth 2.0 and OpenID Connect deployments come not from the protocols but from the decisions made by the implementer. This article collects the error classes that keep appearing in the same places, why they arise, and how they are closed.

10 min read Error classes

The conceptual error: authorisation mistaken for authentication

OAuth 2.0 is an authorisation framework: it issues a token granting a client limited access to a user's resources. Telling you who logged in is not its job. OpenID Connect adds an identity layer on top of it with the id_token.

The most expensive mistakes start when that distinction disappears. The classic example: an application takes an access_token supplied by a client, asks the provider's profile endpoint about it, and signs the user in based on the identity returned — without validating to whom and for which client the token was issued. In that case an attacker can take a valid token issued for an application under their own control and use it to sign in as the victim. It is the textbook confused deputy.

The rule: use the id_token for identity, and validate its signature and its aud claim. If you do accept an access token as proof of identity, you must at minimum use token introspection to check which client it was issued to.

Flow selection and inherited risk

Today's correct default is simple: Authorization Code + PKCE, for every client type, including confidential server-side clients. The legacy flows leave the following behind:

  • Implicit flow: the token is returned directly in the fragment of the redirect URL. Browser history, referrer headers, intermediate logs and third-party scripts on the page can all touch it. It is being removed in the direction of OAuth 2.1.
  • Resource owner password credentials: requires the client to see the user's password; incompatible with multi-factor authentication and federation. Should be considered abandoned.
  • Device authorization flow: the right answer in its niche, but exposed to phishing scenarios that talk a user into approving a code. Device binding and clear context shown to the user are essential.

redirect_uri: the most productive error class

The authorization server must send the code only to a registered address. The moment the matching logic loosens, the code becomes movable to an address the attacker controls. Recurring forms of that looseness:

  • Prefix matching: the registered value is treated as a prefix and anything appended is accepted.
  • Wildcard subdomains: all subdomains allowed, so one weak subdomain is enough.
  • Normalisation mismatch: matching and the actual redirect apply different path normalisation.
  • Query/fragment flexibility: extra parameters or fragments can be appended to the registered address.
  • A chained open redirect: if a registered, entirely "legitimate" address contains an open redirect, the code arrives there and is then carried onwards. This is exactly why open redirect findings should not be dismissed as low severity in applications that use OAuth.
Registered: https://app.example.tld/oauth/callback

Must be rejected:
  https://app.example.tld/oauth/callback/../../elsewhere
  https://app.example.tld/oauth/callback?next=https://other.tld
  https://app.example.tld.attacker.tld/oauth/callback
  https://anything.app.example.tld/oauth/callback

Must be accepted:
  https://app.example.tld/oauth/callback   (exact match)

The defensive side is short: exact string matching, a pre-registered allowlist, no dynamic parts. A separate list per client, separation of production and test environments, and pruning unused registrations belong to the same heading.

state and PKCE: two parameters doing two different jobs

A common shortcut: "we have PKCE, so we don't need state." They defend against different attacks.

  • state — client-side CSRF and session-binding protection. Without it an attacker can have an authorization code belonging to their own account completed inside the victim's browser. The result: the victim's session in the application is bound to the attacker's identity and whatever the victim enters flows into the attacker's account. In social account-linking flows the reverse also occurs and leads directly to account takeover.
  • PKCE (code_verifier / code_challenge) — prevents an intercepted authorization code from being redeemed by someone else. Mandatory for public clients, recommended for all.
  • nonce (OIDC) — protects against id_token replay. It does not substitute for state.

The implementation details need auditing too: state must be unguessable, bound to the session, single use, and actually compared on return — merely checking that it exists is a common bug. On the PKCE side, accepting plain as the code_challenge_method largely defeats the mechanism; only S256 should be accepted. Authorization codes must be single use and short lived, and a second redemption attempt should revoke the associated tokens.

Token validation mistakes

The error classes in JWT-based authentication are well standardised by now, and still show up regularly.

  • Not verifying the signature. Decoding the token and trusting the claims inside it. Confusing a library's "decode" and "verify" functions is the most common form.
  • Trusting the alg header. Accepting none, or allowing asymmetric/symmetric confusion — the server must take the expected algorithm from configuration, not from the token.
  • Letting the token choose the key source. Rather than trusting key-location headers, use keys fetched and cached from the provider's known JWKS endpoint.
  • Not checking the claims. Is iss the expected provider, is aud this client, is exp in the past, does nonce match what was sent, is azp the expected client? Skipping the aud check is the key to the confused deputy scenario above.
  • Deriving user identity from the wrong claim. The stable identifier is the iss + sub pair. Matching on the email claim leads to account merging when an email is unverified or a provider changes.

Mix-up attacks and multi-provider setups

A client supporting more than one identity provider must know with certainty which provider a response came from. Otherwise an attacker can start the flow via their own provider and have the code delivered to the legitimate provider's token endpoint — or conversely, carry the client's secret to an endpoint they control.

The protection is carrying the issuer identity in the authorization response (the iss parameter) and having the client compare it against the provider it expected. Using a distinct redirect_uri per provider is also a practical and effective separation.

Scope, consent and refresh tokens

  • Scope must not live only in the request. The resource server should re-validate the token's scope on every request. "The client asked for this scope" is not an authorisation decision.
  • Do not skip consent. Auto-approval for first-party applications is common, but if it also kicks in silently for scope-upgrade requests, the user has granted a permission they never saw.
  • Rotate refresh tokens. For public clients, rotation with reuse detection is essential: if the same refresh token is presented twice, the whole family should be revoked. Otherwise a stolen refresh token means indefinite access.
  • What logout means. If signing out of the application does not revoke the token, the feeling of "I logged out" diverges from the actual access state. Token revocation and session termination must be handled separately.

Token storage and leakage paths

Getting the protocol right and then storing the token in the wrong place gives the security back.

  • A token kept in localStorage in the browser is readable by any script on the page; a single XSS finding turns directly into account takeover. Prefer cookies with HttpOnly, Secure and an appropriate SameSite setting, or a backend-for-frontend pattern that never sends the token to the browser at all.
  • Authorization codes and tokens can land in access logs, error traces, analytics events and Referer headers. Avoiding tokens in query parameters is the baseline measure.
  • On mobile and desktop clients, custom scheme callbacks can also be registered by other applications. Verified app links and PKCE should be used together.

Triage checklist

Control What it prevents
Exact string matching of redirect_uri, no wildcardsCode leakage, account takeover
No open redirect on the registered callbackCode carried out via a chain
state generated, session-bound and compared on returnCSRF, code injection, account linking
PKCE mandatory, S256 onlyRedemption of an intercepted code
Authorization code single-use and short-livedReplay
id_token signature, iss, aud, exp, nonce validatedForged identity, confused deputy
Algorithm taken from configuration, not from the tokenalg confusion
Identity keyed on iss + subAccount merging
Scope re-validated at the resource serverPrivilege overreach
Refresh token rotation with reuse detectionPersistent access
Tokens not stored where page scripts can read themTakeover via XSS

Summary

Looking for vulnerabilities in OAuth 2.0 and OIDC usually means testing assumptions rather than breaking cryptography: does the server really match redirect_uri exactly, is state really compared, is aud really checked, is scope really re-validated at the resource. Each of those questions is answered by a single line of code, and skipping any of them tends to end in the same place: account takeover.

The good news is that the defensive list is short and largely standardised. The IETF security best-practice document and the direction of OAuth 2.1 already describe the right defaults; the work is verifying that an implementation actually applies them.

References

  1. RFC 6749 — The OAuth 2.0 Authorization Framework.
  2. RFC 6819 — OAuth 2.0 Threat Model and Security Considerations.
  3. RFC 7636 — Proof Key for Code Exchange (PKCE).
  4. RFC 8252 — OAuth 2.0 for Native Apps.
  5. RFC 9207 — OAuth 2.0 Authorization Server Issuer Identification.
  6. RFC 9700 — OAuth 2.0 Security Best Current Practice.
  7. OpenID Connect Core 1.0 — OpenID Foundation.
  8. OWASP API Security Top 10: owasp.org/API-Security