~/learn / authorization / oauth

OAuth 2.0 Explained

How OAuth lets one app act on your behalf at another without your password, the authorization code flow and PKCE explained mechanically, how redirect and state attacks steal accounts, and the misconfigurations testers find.

intermediateupdated

When an app offers “connect your Google account” or “sign in with GitHub,” it is using OAuth. OAuth solves one problem: letting an application access your data at another service without ever seeing your password. The key word is delegation. OAuth grants limited, revocable access; it does not, by itself, prove identity. Identity is layered on top by OpenID Connect, covered in single sign-on.

The four roles

Role Who it is Example
Resource owner You, the user You
Client The app that wants access A calendar app
Authorization server Issues tokens after you approve Google’s login and consent screen
Resource server Holds the data, accepts tokens The Google Calendar API

OAuth is a textbook confused-deputy setup: the client is a deputy you grant limited authority. Every attack below is a way to redirect that authority to the wrong party.

The authorization code flow with PKCE

This is the flow to expect for nearly everything today, including single-page and mobile apps.

  1. The client generates a random code verifier (a high-entropy string) and derives a code challenge from it: challenge = base64url(SHA-256(verifier)).
  2. The client sends your browser to the authorization server with its client_id, a redirect_uri, the requested scope, a random state, and the code_challenge.
  3. You log in at the authorization server, not at the client, and approve the scopes.
  4. The authorization server redirects your browser back to the registered redirect_uri with a short-lived authorization code and the same state.
  5. The client verifies state matches, then exchanges the code plus the original code verifier, server to server, for an access token.
  6. The client calls the API with Authorization: Bearer <access token>.

Two pieces do the security work, and understanding exactly what each prevents is the whole topic.

  • state binds the redirect back to the request your browser started. Without it, an attacker can complete their own authorization, capture their code, and trick your browser into redeeming it, logging you into the attacker’s account (login CSRF). With a random state the client checks, the forged response does not match and is rejected.
  • PKCE proves that whoever redeems the code is whoever began the flow. The authorization server remembers the code_challenge and, at token exchange, requires the matching code_verifier. Since SHA-256 is one-way, an attacker who intercepts the code (on a mobile device, through a logged redirect, through a malicious app registered for the same URL scheme) still cannot produce the verifier, so the stolen code is worthless.

The redirect_uri attack, step by step

The redirect_uri is where the code is delivered, which makes it the prize. If the authorization server validates it loosely, the attacker wins:

  • Open redirect on the client. The attacker registers https://client.test/callback, then abuses an open redirect on that domain so the code bounces onward to attacker.test. Exact-match registration of the full path blunts this; eliminating open redirects on the client domain closes it.
  • Substring or prefix matching. A server that accepts any URI “starting with” the registered value accepts https://client.test.attacker.test/. Validation must be exact string equality against a registered allowlist, not prefix, not “same domain,” not regex.
  • Extra parameters or fragments. Appending ?x= or # to slip past a naive check. Exact match again.

Because the code alone is often enough to log in as the victim at a client that lacks PKCE, redirect validation plus PKCE together are what make the flow safe.

Other flows you will meet

  • Client credentials. Machine to machine, no user. A backend gets a token to call another service on its own behalf.
  • Device authorization. For TVs and CLIs: the device shows a code, you approve on your phone.
  • Refresh tokens. Long-lived tokens that mint new access tokens without re-login. They must be stored carefully, bound to the client, and rotated so that a stolen refresh token is detected (reuse of a rotated token signals theft).
  • Implicit flow and password grant. Both deprecated. RFC 9700 (OAuth 2.0 Security Best Current Practice) says do not use them; the implicit flow returned tokens in the URL fragment where they leaked, and the password grant hands the client your actual password, defeating the point. Finding either in an assessment is worth reporting.

Scopes limit, they do not authorize

A scope such as calendar.read limits what a token may be used for. It does not decide whether this user may read this specific calendar. The resource server must still check object-level ownership on every request. A valid, correctly-scoped token is necessary, not sufficient, which is the same lesson as everywhere else in authorization.

What testers look for

Every one of these is testable on PortSwigger’s free OAuth labs.

Weakness Why it matters Fix
Loose redirect_uri validation Codes or tokens diverted to an attacker URL Exact-match against registered URIs
Missing or unchecked state Login CSRF: victim logged into the attacker’s account Generate and verify state
No PKCE for public clients Intercepted codes can be redeemed Require PKCE for every client
Tokens in URLs Leak via logs, history, Referer Code flow only, never tokens in query strings
Over-broad scopes One compromised client reaches everything Request the minimum scope
Long-lived, unrotated refresh tokens A single leak grants lasting access Rotation, expiry, client binding

Field notes: stealing the authorization code

Most OAuth account takeovers come down to redirecting the code to a URL you control. The redirect_uri validation is the battleground, and lazy matching loses:

Server’s matching rule Bypass
Prefix match redirect_uri=https://app.test.attacker.com
Substring / contains redirect_uri=https://attacker.com/app.test
Allows subpaths an open redirect on the real app: .../callback?next=//attacker.com
Allows subdomains any XSS or takeover on a sibling subdomain
Ignores fragment/params append &redirect_uri=... a second time (parameter pollution)

Even with a locked redirect_uri, the code can leak through the Referer header (if the callback page loads attacker-controlled resources) or through an open redirect in the allowed path. Once you have a victim’s code, you redeem it at the token endpoint and you are them.

Field notes: state, PKCE, and scope

  • Missing state is CSRF on the login flow: you start an OAuth flow, capture your code, and trick the victim’s browser into completing the callback with it, logging the victim into your account (or, in the “login CSRF” direction, linking your social account to their session). Always check whether state is present, unpredictable, and actually validated (remove it and see if the flow still completes).
  • PKCE downgrade: a public client should send code_challenge. If the server still issues a token when you omit it, PKCE is not enforced and a stolen code is enough. Try stripping code_challenge and code_verifier.
  • Scope does not authorize. A token minted for read scope that the API honors for a write is a classic broken-authorization bug. Request minimal scope, then try privileged actions.
  • response_type=token (implicit flow) puts the token in the URL fragment where it leaks through history, Referer, and redirects. Its presence on a modern app is itself a finding; prefer the code flow with PKCE.

The highest-impact OAuth bug is full account takeover by email-claim trust: if the app creates or links accounts by the email in the provider’s profile and you can control an unverified email at an attacker-run or misconfigured IdP, you log in as the victim. Only test flows you are authorized to.

Takeaway

OAuth moves the password prompt to a trusted authorization server and hands the client a limited, revocable token. Almost no real-world OAuth bug is in the protocol itself. They are in how a specific client or server validates redirects, state, PKCE, and tokens, which is exactly where you look.