OAuth Is Authorization, Not Authentication

This is the single most common confusion. OAuth 2.0 answers the question "can this application access this resource on the user's behalf?" It was never designed to answer "who is this user?" A client can complete a valid OAuth flow and receive an access token without the application ever learning the user's identity in a standardized way.

OpenID Connect (OIDC) was built on top of OAuth 2.0 specifically to close this gap. It adds a new token type — the ID token, a signed JWT containing identity claims — and a standard /userinfo endpoint. If your application needs to know who logged in, you need OIDC, not bare OAuth 2.0. Most "Login with X" buttons on the web today are OIDC, even though people colloquially call it "OAuth login."

The Four Roles

Every OAuth flow involves the same four parties, defined precisely in RFC 6749 §1.1:

RoleWhat it isExample
Resource OwnerThe user who owns the data and can grant access to itYou, logging into a photo-printing app
ClientThe application requesting access on the user's behalfThe photo-printing app
Authorization ServerIssues tokens after authenticating the user and getting consentGoogle Accounts, Auth0, Okta, your own auth service
Resource ServerHosts the protected data and accepts the access tokenGoogle Photos API

The Authorization Code Flow (with PKCE)

This is the flow you should default to for nearly everything — server-side web apps, single-page apps, and native/mobile apps. It never exposes the access token to the browser's URL bar or history.

1. Client generates a random code_verifier, derives code_challenge = BASE64URL(SHA256(code_verifier))
2. Client redirects the user to the Authorization Server:
   GET /authorize?
       response_type=code
       &client_id=abc123
       &redirect_uri=https://app.example.com/callback
       &scope=openid profile photos.read
       &state=xk9F2mZq          // random, unguessable — CSRF protection
       &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
       &code_challenge_method=S256
3. User authenticates and approves the requested scopes
4. Authorization Server redirects back:
   GET /callback?code=SplxlOBeZQQYbYS6WxSbIA&state=xk9F2mZq
5. Client verifies the returned state matches what it sent, then exchanges the code:
   POST /token
       grant_type=authorization_code
       &code=SplxlOBeZQQYbYS6WxSbIA
       &redirect_uri=https://app.example.com/callback
       &client_id=abc123
       &code_verifier=<the original, un-hashed value from step 1>
6. Authorization Server verifies SHA256(code_verifier) == code_challenge, then returns:
   { "access_token": "...", "refresh_token": "...", "expires_in": 3600, "token_type": "Bearer" }
7. Client calls the Resource Server with: Authorization: Bearer <access_token>
⚠️ Why PKCE Matters

Step 4 sends the authorization code through the browser's redirect — on mobile, this can be intercepted by a malicious app registered for the same custom URL scheme. Without PKCE, whoever intercepts the code can exchange it for tokens. With PKCE, the interceptor is missing the code_verifier — a value that never left the legitimate client — so the exchange fails. OAuth 2.1 makes PKCE mandatory for every client, confidential or public.

Client Credentials Flow (Machine-to-Machine)

When there's no user in the loop — a backend service calling another service's API — the client authenticates directly with its own credentials:

POST /token
    grant_type=client_credentials
    &client_id=service-a
    &client_secret=<kept server-side, never exposed>
    &scope=orders.read orders.write

The response is an access token scoped to the client's own permissions — there is no refresh token, because there's no user session to extend; the service simply requests a new token when the old one expires.

Refresh Tokens and Rotation

Access tokens are deliberately short-lived (5–60 minutes) so a leaked token has a small blast radius. Refresh tokens are long-lived and exist to get a new access token without forcing the user to log in again:

  1. Client sends the refresh token to /token with grant_type=refresh_token.
  2. Authorization Server issues a new access token — and, with rotation enabled, a new refresh token, invalidating the old one.
  3. If a refresh token is used twice (the old one, after rotation), the server treats this as a signal of theft and can revoke the entire token family.
✅ Storage Best Practice

For browser-based apps, store tokens in httpOnly, Secure, SameSite=Strict cookies, not localStorage or sessionStorage. Any XSS vulnerability on the page can read localStorage; it cannot read an httpOnly cookie. This is the same guidance that applies to JWTs generally — see our JWT deep dive for the token-storage tradeoffs in detail.

Scopes: Asking for Only What You Need

Scopes are space-delimited strings the client requests and the user approves — photos.read, orders.write, openid profile email. The authorization server can grant fewer scopes than requested; the client must check the scope value actually returned rather than assuming it got everything it asked for. Requesting broad scopes ("just ask for everything, we might need it later") increases the damage a leaked token can do and is flagged in most security reviews — request the minimum a feature needs, expand later if required.

OAuth 2.0 vs OpenID Connect: What's the Actual Difference

DimensionOAuth 2.0OpenID Connect
PurposeAuthorization — delegated access to resourcesAuthentication — verifying who the user is, built on OAuth 2.0
Token issuedAccess token (often opaque, not necessarily a JWT)Access token + ID token (always a signed JWT)
Identity claimsNot standardizedsub, email, name, etc. via /userinfo or the ID token
Required scopeWhatever the API definesMust include openid
Spec bodyIETF, RFC 6749OpenID Foundation, built on top of RFC 6749

In practice: if your application only calls an API on behalf of a user and never needs to know who that user is by name or email, that's plain OAuth 2.0. If you show "Welcome, Alice" anywhere, you're using OIDC, and the ID token — a JWT you can decode with the same structure covered in our JWT debugger — is where that identity data lives.

Flows You Should Avoid

FlowStatusWhy it's deprecated
Implicit FlowRemoved in OAuth 2.1Returns the access token directly in the URL fragment — exposed to browser history, referrer leaks, and any script on the page.
Resource Owner Password CredentialsRemoved in OAuth 2.1Requires the client to collect the user's raw password directly, defeating the entire purpose of delegated authorization.

Both were part of OAuth 2.0's original 2012 spec for use cases that no longer need them — the authorization code flow with PKCE now covers browser and native apps without their tradeoffs. OAuth 2.1 is, as of mid-2026, still an active IETF working-group draft rather than a published RFC — it consolidates OAuth 2.0 plus the security best-practice extensions (PKCE, no implicit flow) that the industry already treats as mandatory in practice.

Vulnerabilities That Show Up in Real Reviews

1. Missing or Predictable state

Without a random, per-request state value, an attacker can initiate their own OAuth flow, capture the resulting authorization code, and trick a victim into completing it via a crafted link — binding the victim's session to the attacker's resource. Verify state on every callback, and generate it with a cryptographically secure random source.

2. Unvalidated redirect_uri

If the authorization server accepts any redirect_uri or only checks a prefix, an attacker can register a lookalike callback URL and redirect the authorization code straight to a server they control. Redirect URIs should be registered exactly, per client, with exact-match validation — no wildcards.

3. Authorization Code Reuse

Authorization codes are meant to be single-use and short-lived (typically under a minute). A server that fails to invalidate a code after its first exchange allows an intercepted code to be replayed. PKCE mitigates interception; single-use enforcement mitigates replay.

4. Overly Broad Scopes Granted Silently

Consent screens that bundle unrelated scopes together, or clients that request scopes far beyond what the feature needs, widen the blast radius of a compromised token. Scope creep is a design decision, not just an implementation detail — review it the same way you'd review a permissions request in a mobile app.

Which Flow Should You Actually Use

Application typeRecommended flow
Server-side web app (has a backend, can keep a secret)Authorization Code + PKCE
Single-page app (SPA, no backend)Authorization Code + PKCE (never implicit)
Native / mobile appAuthorization Code + PKCE
Backend service calling another backend serviceClient Credentials
Smart TV / device with no browserDevice Authorization Grant (RFC 8628)