OAuth 2.0 is one of those technologies developers use constantly but understand only at the surface level. "Sign in with Google" works, tokens get passed around, something gets refreshed — but the actual flow underneath is worth knowing. Especially because OAuth is widely misunderstood as an authentication system when it's actually about authorization.
What Problem OAuth Solves
Before OAuth, the way apps accessed resources on your behalf was simple and terrible: you gave them your username and password. A third-party app that needed to read your Gmail stored your Google credentials. If it got compromised, your credentials were exposed. If you wanted to revoke access, you had to change your password — breaking every other app in the process.
OAuth solves this with delegated authorization. Instead of sharing credentials, you authorize an application to access specific resources on your behalf, and the authorization server issues tokens that represent that permission. The app gets a scoped, time-limited credential, not your actual password.
The Four Roles
flowchart LR RO((Resource Owner<br/>"the user")) C[Client<br/>"YourApp"] AS[Authorization Server<br/>"accounts.google.com"] RS[Resource Server<br/>"Google Photos API"] RO -- "1. authenticates &<br/>grants consent" --> AS C -- "2. exchanges code<br/>for token" --> AS AS -- "3. issues access_token" --> C C -- "4. Bearer access_token" --> RS RS -- "5. protected resource" --> C classDef u fill:#1f1f1f,stroke:#a78bfa,color:#e4e4e4; classDef s fill:#1f1f1f,stroke:#60a5fa,color:#e4e4e4; classDef c fill:#1f1f1f,stroke:#f5c842,color:#e4e4e4; classDef r fill:#1f1f1f,stroke:#4ade80,color:#e4e4e4; class RO u class C c class AS s class RS r
OAuth 2.0 defines four participants:
Resource Owner — the user. The person who owns the data and grants access to it.
Client — the application requesting access. This could be a web app, mobile app, or backend service.
Authorization Server — the server that authenticates the user and issues tokens. Google's OAuth server, GitHub's OAuth server, your own auth service.
Resource Server — the API that hosts the protected resources. The Google Photos API, GitHub API, etc. Often the same infrastructure as the authorization server, but conceptually distinct.
The Authorization Code Flow
sequenceDiagram
participant U as User (browser)
participant C as Client (yourapp.com)
participant AS as Authorization Server<br/>(accounts.google.com)
participant RS as Resource Server<br/>(API)
U->>C: Click "Sign in with Google"
C->>U: 302 → /authorize?client_id=...&state=...&scope=...
U->>AS: GET /authorize
AS->>U: Login + consent screen
U->>AS: Approve
AS->>U: 302 → /callback?code=abc&state=...
U->>C: GET /callback?code=abc&state=...
C->>C: Verify state matches stored value
C->>AS: POST /token (code, client_id, client_secret)
AS->>C: { access_token, refresh_token, expires_in }
C->>RS: GET /userinfo<br/>Authorization: Bearer access_token
RS->>C: 200 { user data }
C->>U: Logged in
This is the most common flow for web and mobile apps where a human user is involved. Here's what happens when you click "Continue with Google" on a third-party site.
Step 1: Redirect to the authorization server
The client redirects your browser to the authorization server with a request:
https://accounts.google.com/o/oauth2/auth
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.com/callback
&scope=openid email profile
&state=random-csrf-token
The state parameter is a random value you generate, stored in session. You'll verify it later to prevent CSRF attacks.
Step 2: User authenticates and consents
The authorization server shows the Google login screen, then the consent screen ("YourApp wants to access your email and profile"). You click Allow.
Step 3: Authorization code returned
The browser redirects back to your redirect_uri with a short-lived authorization code:
https://yourapp.com/callback?code=4/P7q7W91azSyVe&state=random-csrf-token
This code is single-use and expires in minutes. It's not an access token — it's a one-time credential for your server to exchange for tokens.
Step 4: Code exchange (server-side)
Your backend server makes a POST request to the token endpoint, authenticating itself with the client secret:
POST https://oauth2.googleapis.com/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=4/P7q7W91azSyVe
&redirect_uri=https://yourapp.com/callback
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
The response includes:
{
"access_token": "ya29.a0AfH6SM...",
"expires_in": 3600,
"refresh_token": "1//04...",
"scope": "openid email profile",
"token_type": "Bearer"
}
The client_secret never leaves your server, which is why this exchange happens server-side. If the client is a browser-only SPA, you don't have a client secret to protect — that's where PKCE comes in.
PKCE for Public Clients
sequenceDiagram
participant C as Public client (SPA / mobile)
participant AS as Authorization Server
C->>C: verifier = random 64 bytes<br/>challenge = SHA256(verifier)
C->>AS: /authorize?code_challenge=<challenge>&method=S256
AS->>AS: Stores challenge against pending code
AS->>C: 302 callback?code=abc
C->>AS: POST /token (code, code_verifier=<verifier>)
AS->>AS: SHA256(verifier) == stored challenge ?
AS->>C: { access_token, ... }
Note over C,AS: An attacker who intercepted "code" alone<br/>still can't redeem it without the verifier
PKCE (Proof Key for Code Exchange, pronounced "pixie") lets public clients — mobile apps, SPAs — use the authorization code flow securely without a client secret.
Instead of a client secret, the client generates a random code_verifier, hashes it to get a code_challenge, and sends the challenge in the initial authorization request:
const verifier = generateRandomString(64);
const challenge = base64url(sha256(verifier));
// Authorization request includes:
// &code_challenge=<challenge>
// &code_challenge_method=S256
During the code exchange, the client sends the original code_verifier. The authorization server hashes it and checks it matches the challenge it stored. An intercepted authorization code is useless without the verifier.
PKCE is now recommended for all clients — not just public ones — per RFC 9700 (OAuth 2.0 Security Best Current Practice). RFC 7636 defines the PKCE mechanism itself.
Access Tokens vs Refresh Tokens
sequenceDiagram
participant C as Client
participant AS as Authorization Server
participant RS as Resource Server
Note over C,RS: t = 0
C->>RS: GET /api/data + Bearer access_token
RS-->>C: 200 OK
Note over C,RS: t = 60 min (token expired)
C->>RS: GET /api/data + Bearer access_token
RS-->>C: 401 Unauthorized
C->>AS: POST /token grant_type=refresh_token
AS-->>C: { new access_token, [new refresh_token] }
C->>RS: GET /api/data + Bearer new access_token
RS-->>C: 200 OK
Access tokens are short-lived credentials (typically 1 hour) that the client presents to the resource server on each request:
GET /api/userinfo
Authorization: Bearer ya29.a0AfH6SM...
They're short-lived by design — they can't be easily revoked, so if one leaks, it expires soon anyway.
Refresh tokens are long-lived credentials that let the client get new access tokens without re-prompting the user. When the access token expires, the client swaps the refresh token for a new one:
POST /token
grant_type=refresh_token
&refresh_token=1//04...
&client_id=YOUR_CLIENT_ID
Refresh tokens can be revoked at the authorization server — this is how "log out everywhere" or "revoke app access" works. They should be stored securely (server-side for web apps, secure storage for mobile).
Other Common Flows
Client Credentials flow — for machine-to-machine access with no user involved. Your backend service authenticates directly with the authorization server using its client ID and secret to get an access token.
POST /token
grant_type=client_credentials
&client_id=SERVICE_ID
&client_secret=SERVICE_SECRET
&scope=api:read
Device Authorization flow — for devices without a browser (smart TVs, CLI tools). The device shows a code like "Go to example.com/activate and enter XXXX-YYYY." The user does this on a phone or computer while the device polls until access is granted.
What OAuth Is NOT
flowchart LR
subgraph OA["OAuth 2.0 — authorization"]
Q1["'Can this app<br/>access my photos?'"]
A1["access_token →<br/>scope: photos.read"]
Q1 --> A1
end
subgraph OIDC["OpenID Connect — identity"]
Q2["'Who is this user?'"]
A2["id_token (JWT)<br/>{ sub, email, name }"]
Q2 --> A2
end
Note["When you 'Sign in with Google',<br/>you are using OIDC, which sits on top of OAuth 2.0"]
OA --- OIDC
classDef a fill:#1f1f1f,stroke:#fb923c,color:#e4e4e4;
classDef b fill:#1f1f1f,stroke:#60a5fa,color:#e4e4e4;
class OA a
class OIDC b
Here's where most confusion lives: OAuth 2.0 is not an authentication protocol.
OAuth tells you that an application has permission to access certain resources on behalf of a user. It doesn't tell you who the user is.
When you "Sign in with Google," you're not using OAuth 2.0 for sign-in — you're using OpenID Connect (OIDC), an identity layer built on top of OAuth 2.0. OIDC adds an id_token (a JWT containing user identity claims) to the standard OAuth flow.
{
"access_token": "ya29.a0AfH6...",
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...", ← this is OIDC
"token_type": "Bearer"
}
The access token says "this app can access these resources." The ID token says "this is who the user is." Different purposes, should be handled differently.
If you're implementing "login with X," you're implementing OIDC, not bare OAuth 2.0. Most identity providers (Google, GitHub, Auth0, Okta) support OIDC on top of their OAuth 2.0 infrastructure.
Inspecting Tokens
Access tokens are often opaque strings (only the issuer can decode them). But many systems issue JWTs — self-contained tokens you can decode locally to read the claims. The JWT Decoder does exactly this without sending the token to any server.
The Base64 Encoder is useful when you're manually constructing or debugging OAuth requests, since client credentials are sometimes passed as base64-encoded Basic Auth headers:
Authorization: Basic base64(client_id:client_secret)
For a deeper look at how tokens are signed and verified, see JWT Tokens Explained. For how TLS protects the token exchange in transit, see How TLS and HTTPS Work.
Security Checklist
A few things worth validating in any OAuth implementation:
- Always verify the
stateparameter on callback to prevent CSRF. - Use PKCE for any public client, regardless of whether a client secret exists.
- Store refresh tokens securely — treat them like passwords.
- Validate the
aud(audience) claim in ID tokens. - Use short-lived access tokens and rely on refresh tokens for continuity.
- Bind redirect URIs strictly — wildcards in redirect URIs are a common misconfiguration that allows authorization code theft.
OAuth 2.0 is a flexible spec — flexible enough that implementations vary significantly. The OAuth 2.0 Security Best Current Practice (RFC 9700) is worth reading if you're implementing an authorization server or writing a non-trivial OAuth client.
FAQ
Is OAuth the same as authentication?
No, and this is the most common misconception. OAuth 2.0 is an authorization protocol — it grants access to resources on behalf of a user. It doesn't tell you who the user is. When you "Sign in with Google," you're using OpenID Connect (OIDC), which adds an id_token identity layer on top of OAuth. Bare OAuth 2.0 for login is incorrect; use OIDC.
Should I still use the implicit flow in 2026?
No. The implicit flow is officially deprecated by RFC 9700. SPAs and mobile apps should use the authorization code flow with PKCE instead — same security as a confidential client without needing a client secret. If you're implementing OAuth from scratch in 2026, skip implicit entirely.
What's the right access token lifetime?
5 minutes to 1 hour for most cases. Short enough that a leaked token's damage is limited; long enough to avoid hammering the token endpoint with refreshes. Banking and healthcare often use 5 minutes; consumer SaaS leans toward 30-60 minutes. Pair short access tokens with refresh tokens that last days or weeks.
Where should I store OAuth tokens in a browser app?
Refresh tokens: never in localStorage — use HttpOnly secure cookies, ideally backed by a same-origin backend that proxies to the OAuth server. Access tokens: in-memory JavaScript variable for the lifetime of the page is the safest, with refresh on reload. localStorage is XSS-vulnerable; sessionStorage is barely better.
Do I need PKCE if I have a client secret?
Yes — RFC 9700 recommends PKCE for all clients, even confidential ones. PKCE protects against authorization code interception attacks that a client secret alone doesn't fully cover. The cost is trivial (one hash, one extra parameter), and it's the modern best practice.
Why does OAuth need a redirect_uri at all?
It's how the authorization code reaches your app — the auth server can't push to it directly. The redirect_uri must be pre-registered with the authorization server and matched exactly during the flow; otherwise an attacker could redirect codes to a malicious URL. Wildcards in redirect_uri configuration are a notorious source of OAuth vulnerabilities.
Can I refresh a refresh token?
Many providers issue refresh token rotation: every time you exchange a refresh token, you get a new one and the old becomes invalid. This limits the damage from a leaked refresh token (the leak is only useful for one round) and lets the auth server detect token theft (when the same refresh token is used twice, both are revoked). Google, Auth0, and Okta all support this.
What's the difference between OAuth 2.0 and OAuth 2.1?
OAuth 2.1 (currently a draft) consolidates the security best practices from RFC 9700 into the core spec: PKCE required for all flows, implicit flow removed, redirect_uri exact matching mandatory, refresh token rotation recommended. If you're implementing OAuth from scratch, follow OAuth 2.1 conventions even though the formal RFC is still in progress.