OAuth 2.0 is how almost every modern application handles "Sign in with Google", third-party API access and single sign-on through OpenID Connect. It's also a protocol where security depends on details: which grant you use, how you check redirect URIs, where tokens are stored. Threat modeling an OAuth integration means knowing where those details have gone wrong before.
A quick map of the roles
- Resource owner: the user.
- Client: the application asking for access (your web app, mobile app or SPA).
- Authorization server: issues codes and tokens (Google, Microsoft Entra ID, Okta, Auth0, Keycloak).
- Resource server: the API that accepts the access token.
- User agent: the browser, which carries redirects between the others and is where most attacks happen.
OAuth 2.0 itself is about authorization: granting an application limited access to an API. OpenID Connect (OIDC) adds an ID token that tells the client who the user is, which is what makes it suitable for login.
The main threats
| Threat | How it works | Primary defence |
|---|---|---|
| Authorization code interception | A malicious app or script captures the code during the redirect and exchanges it first | PKCE on every client |
| Redirect URI manipulation | Loose matching (prefixes, wildcards, open redirects on the client) sends codes or tokens to an attacker's page | Exact string matching of pre-registered redirect URIs |
| Token leakage | Tokens in URLs leak via history, referrer headers, logs and proxies; tokens in browser storage are readable by XSS | No implicit grant; tokens never in URLs; short-lived access tokens |
| CSRF on the redirect (login CSRF) | An attacker makes the victim's browser complete a flow with the attacker's code, logging the victim into the attacker's account | PKCE, or a one-time state value (OIDC: nonce) bound to the user's session |
| Mix-up attack | A client using several authorization servers is tricked into sending one server's code to another | Check the iss parameter in the response (RFC 9207) |
| Stolen refresh tokens | A long-lived token gives persistent access after the user's session ends | Refresh token rotation, or sender-constrained tokens |
| Token replay at another API | A token issued for one resource server is accepted by another | Audience restriction (aud) checked by every resource server |
| Over-broad scopes | A compromised client holds far more access than it needs | Least-privilege scopes; consent screens users can understand |
The current baseline: RFC 9700
The original OAuth 2.0 threat model, RFC 6819, dates from 2013. Its successor, RFC 9700, Best Current Practice for OAuth 2.0 Security, was published in January 2025 and is now the reference. Its key requirements:
- Use the authorization code grant with PKCE for all clients, public and confidential.
- Don't use the implicit grant (tokens in the URL fragment) or the resource owner password credentials grant (the client collects the user's password).
- Match redirect URIs exactly, with no wildcards or pattern matching (localhost port numbers for native apps are the narrow exception).
- Don't put access tokens in URLs.
- Restrict tokens to a specific audience and the minimum scopes, and consider sender-constrained tokens (mutual TLS or DPoP) so a stolen token can't be replayed by someone else.
- Rotate refresh tokens for public clients, or sender-constrain them.
The draft OAuth 2.1 specification consolidates these practices into the core protocol.
Worked example: Invoicely
Suppose Invoicely adds "Sign in with Google" and lets accounting tools connect to its API through OAuth. Two attack trees come out of the threat model:
- "Log in as another customer": leaves include redirect URI manipulation through an open redirect on
invoicely.example/go?url=, login CSRF with a missingstatecheck, and account linking by unverified email address. The last one is the subtle one: if Invoicely links a Google login to an existing account just because the email matches, it must check the identity provider has verified that email. - "Use a stolen API token": a third-party integration's refresh token leaks from its database. Scoped tokens (
invoices:readonly), refresh token rotation and a per-integration revoke button cut the impact from "full account" to "read invoices until revoked".
Our guide to threat modeling an SSO integration goes deeper into the login side.
Modeling OAuth on a data flow diagram
- Draw the browser as its own element. Every redirect passes through it, and it's where codes and tokens are exposed.
- Put each party in its own trust boundary: user agent, client backend, authorization server, resource server.
- Label each flow with what it carries: authorization request, code, token request, access token, refresh token, ID token.
- For each token, ask where it's stored, who can read it, how long it lives and how it's revoked. Store refresh tokens like secrets.
Common mistakes
- Using an ID token to call APIs. ID tokens are for the client to learn who the user is; access tokens are for APIs.
- Not validating tokens properly. Check signature, issuer, audience and expiry, and pin the accepted signing algorithms.
- Trusting the email claim for account linking without checking that the provider verified it.
- Long-lived access tokens in browser storage, where any XSS can read them.
Key takeaways
- OAuth 2.0 delegates authorization; OpenID Connect adds authentication. Most OAuth attacks steal codes or tokens in transit through the browser.
- RFC 9700 (January 2025) is the current baseline: authorization code flow with PKCE for every client, exact redirect URI matching, no implicit or password grants.
- The redirect URI is the most attacked part of OAuth. Exact string matching closes most code and token theft.
- Treat tokens like passwords: short-lived access tokens, sender-constrained where possible, never in URLs or logs.
Frequently asked questions
Is OAuth 2.0 secure?
OAuth 2.0 can be implemented securely, but the original 2012 specification left many choices open, and several of its flows are now considered unsafe. Following RFC 9700 (authorization code with PKCE, exact redirect URI matching, no implicit or password grant) removes the most common weaknesses.
What is PKCE and why is it required?
Proof Key for Code Exchange (RFC 7636) binds an authorization code to the client that requested it. The client sends a hash of a random secret with the authorization request and the secret itself when exchanging the code, so a stolen code is useless on its own. RFC 9700 recommends it for all clients, including confidential ones.
Why is the implicit grant deprecated?
The implicit grant returns the access token directly in the browser's URL fragment, where it can leak through browser history, referrer headers and injected scripts, and it cannot be bound to the client. The authorization code flow with PKCE gives single-page apps the same convenience without those exposures.
What is an OAuth mix-up attack?
An attack on clients that work with more than one authorization server, where the attacker tricks the client into sending a code or token issued by an honest server to a server the attacker controls. It is prevented by checking the iss (issuer) parameter in the authorization response, as described in RFC 9207.