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
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
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
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
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
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.