You've probably clicked "Sign in with Google" or "Continue with GitHub" hundreds of times. Those buttons kick off a carefully choreographed dance between three parties: you, your app, and the providerWhat is provider?A wrapper component that makes data available to all components nested inside it without passing props manually.. That dance is OAuth 2.0What is oauth?An authorization protocol that lets users grant a third-party app limited access to their account on another service without sharing their password., and understanding it is essential for building any modern web application that touches third-party accounts or APIs.
The problem OAuthWhat is oauth?An authorization protocol that lets users grant a third-party app limited access to their account on another service without sharing their password. solves
Before OAuth, if a fitness app wanted to read your Google Calendar, it would ask for your Google password directly. You'd hand it over, hoping the app stores it safely, and of course many did not. OAuth replaces that risky password hand-off with a delegated permission model.
Think of OAuth like a hotel key card system. The hotel (Google) gives you a temporary key card (access tokenWhat is token?The smallest unit of text an LLM processes - roughly three-quarters of a word. API pricing is based on how many tokens you use.) that only opens specific doors (scopes). You hand that card to the valet (your fitness app). The valet can open the car park door but cannot get into your room. When the card expires, the valet has to come back to the front desk.
The authorizationWhat is authorization?Checking what an authenticated user is allowed to do, like whether they can delete records or access admin pages. code flow
The authorization code flow is the gold standard for server-side web apps. It keeps your client secret off the browser entirely. Here is the full sequence:
Step 1: User clicks "Login with Google" on your app
Step 2: Your app redirects to Google: /oauth/authorize?client_id=...
Step 3: User authenticates with Google and approves the requested permissions
Step 4: Google redirects back to your callback URL with a short-lived authorization code
Step 5: Your server exchanges that code for tokens (server-to-server, not via browser)
Step 6: You receive an access_token and optionally a refresh_token
Step 7: Use the access_token to call APIs or fetch user profile informationThe critical insight is step 5: the code exchange happens server-to-server. Your client secret never leaves your server, and the authorization code is useless without it.
Key components
Every OAuthWhat is oauth?An authorization protocol that lets users grant a third-party app limited access to their account on another service without sharing their password. flow involves a predictable set of moving parts. Getting comfortable with these terms will make the documentation for any providerWhat is provider?A wrapper component that makes data available to all components nested inside it without passing props manually. much easier to read.
| Component | What it is | Example |
|---|---|---|
| Client ID | Public identifier for your registered app | 123456.apps.googleusercontent.com |
| Client secret | Secret key used during token exchange | Never expose this to clients |
| Authorization code | Temporary code Google returns to your callback | Valid for roughly 10 minutes, single use |
| Access token | Short-lived credential for making API calls | Typically expires in 1 hour |
| Refresh token | Long-lived credential for getting new access tokens | Valid until the user revokes it |
| Redirect URI | URL the provider sends the user back to | https://myapp.com/auth/callback |
| Scope | The specific permissions you are requesting | openid email profile |
| State | Random value for CSRF protection | A 32-byte hex string you generate |
Building the authorizationWhat is authorization?Checking what an authenticated user is allowed to do, like whether they can delete records or access admin pages. URL
When a user clicks "Sign in with Google", you redirect them to Google's authorization endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users. with a set of query parameters. Here is how you construct that URL:
const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', 'https://myapp.com/auth/callback');
authUrl.searchParams.set('response_type', 'code'); // Request authorization code
authUrl.searchParams.set('scope', 'openid email profile'); // What you want access to
authUrl.searchParams.set('state', generateRandomState()); // CSRF protection - store this in session
authUrl.searchParams.set('access_type', 'offline'); // Request a refresh token too
// Redirect user to authUrl.toString()state value in the user's session before redirecting. When Google sends the user back to your callback, compare the returned state against what you stored. If they do not match, reject the request immediately, it could be a CSRF attack.Redirect URI validation
Redirect URIs are one of the most important security controls in OAuthWhat is oauth?An authorization protocol that lets users grant a third-party app limited access to their account on another service without sharing their password.. The providerWhat is provider?A wrapper component that makes data available to all components nested inside it without passing props manually. compares the redirect_uri you send in the authorizationWhat is authorization?Checking what an authenticated user is allowed to do, like whether they can delete records or access admin pages. request against the list you registered in their developer console. The comparison is exact, no fuzzy matching.
// Registered: https://myapp.com/auth/callback
// These will FAIL:
https://myapp.com/auth/callback?extra=param // Extra query parameter
https://myapp.com/auth/callback/ // Trailing slash
http://myapp.com/auth/callback // HTTP instead of HTTPS
// This will SUCCEED:
https://myapp.com/auth/callbackThis strictness is intentional. If partial matching were allowed, an attacker could redirect the authorization code to their own server by adding a crafted path suffix.
Exchanging the code for tokens
Once your callbackWhat is callback?A function you pass into another function to be called later, often when an operation finishes or an event occurs. URL receives the authorizationWhat is authorization?Checking what an authenticated user is allowed to do, like whether they can delete records or access admin pages. code, you have a narrow window to exchange it for tokens. This must happen server-side.
// Server-side only - never run this in the browser
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
code: authorizationCode,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET, // Never expose this client-side
redirect_uri: 'https://myapp.com/auth/callback',
grant_type: 'authorization_code'
})
});
const tokens = await response.json();
// { access_token, refresh_token, expires_in, token_type }redirect_uri in the token exchange must match exactly what you used in the authorization request. It is not used for redirecting at this stage, it is just a verification check.PKCEWhat is pkce?Proof Key for Code Exchange - an extension to OAuth that secures the authorization flow for apps that can't safely store a client secret, like SPAs and mobile apps. for SPAs and mobile apps
Single-page apps and mobile apps are called "public clients" because they cannot safely store a client secret, anyone can open DevTools or reverse-engineer the binaryWhat is binary?A ready-to-run file produced by the compiler. You can send it to any computer and it just works - no install needed. and find it. PKCE (Proof Key for Code Exchange, pronounced "pixie") solves this problem without needing a secret at all.
The idea: before redirecting, generate a random string called the code_verifier. Hash it to create the code_challenge. Send the hash to the providerWhat is provider?A wrapper component that makes data available to all components nested inside it without passing props manually.. Later, when exchanging the code, send the original verifier. The provider hashes it and checks that it matches, proving you are the same client that started the flow.
// 1. Generate PKCE parameters (client-side, before redirecting)
const codeVerifier = generateRandomString(128); // Store this in sessionStorage
const codeChallenge = base64UrlEncode(sha256(codeVerifier));
// 2. Include in the authorization request
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
// 3. When exchanging the code, send the original verifier (not the hash)
body: new URLSearchParams({
code: authorizationCode,
client_id: CLIENT_ID,
code_verifier: codeVerifier, // Provider hashes this and compares to challenge
grant_type: 'authorization_code'
})OpenID ConnectWhat is openid connect?A layer on top of OAuth 2.0 that adds user authentication, returning an ID token with verified identity information like email and name. and authenticationWhat is authentication?Verifying who a user is, typically through credentials like a password or token.
OAuth 2.0What is oauth?An authorization protocol that lets users grant a third-party app limited access to their account on another service without sharing their password. gives you an access tokenWhat is token?The smallest unit of text an LLM processes - roughly three-quarters of a word. API pricing is based on how many tokens you use. to call APIs. But how do you know who the user is? That is where OpenID Connect (OIDC) comes in. When you include openid in your scopes, the providerWhat is provider?A wrapper component that makes data available to all components nested inside it without passing props manually. returns an additional id_token alongside the access token.
The id_token is a JWTWhat is jwt?JSON Web Token - a self-contained, signed token that carries user data (like user ID and role). The server can verify it without a database lookup. that contains verified user identity information like their email, name, and a unique user ID (sub). You can decode it client-side to get the user's profile without making an extra APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. call.
Quick reference
| Flow | Best for | Needs client secret? | Needs PKCE? |
|---|---|---|---|
| Authorization code | Server-side web apps | Yes | Optional |
| Authorization code + PKCE | SPAs, mobile apps | No | Yes |
| Client credentials | Machine-to-machine (no user) | Yes | No |
| Implicit (deprecated) | Old SPAs, avoid this | No | No |
// Complete OAuth 2.0 flow implementation
import crypto from 'crypto';
// Step 1: Generate authorization URL
function getGoogleAuthUrl() {
const state = crypto.randomBytes(32).toString('hex');
const params = new URLSearchParams({
client_id: process.env.GOOGLE_CLIENT_ID,
redirect_uri: `${process.env.APP_URL}/auth/google/callback`,
response_type: 'code',
scope: 'openid email profile',
state: state,
access_type: 'offline',
prompt: 'consent' // Force consent screen to get refresh token
});
// Store state in session for CSRF protection
session.oauthState = state;
return `https://accounts.google.com/o/oauth2/v2/auth?${params}`;
}
// Step 2: Handle callback and exchange code
async function handleGoogleCallback(code, state) {
// Verify state to prevent CSRF
if (state !== session.oauthState) {
throw new Error('Invalid state parameter');
}
// Exchange code for tokens
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
code,
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
redirect_uri: `${process.env.APP_URL}/auth/google/callback`,
grant_type: 'authorization_code'
})
});
if (!tokenResponse.ok) {
throw new Error('Failed to exchange code for tokens');
}
const tokens = await tokenResponse.json();
// Step 3: Fetch user info with access token
const userResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: { Authorization: `Bearer ${tokens.access_token}` }
});
const userInfo = await userResponse.json();
return {
user: userInfo,
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token, // May be null if already authorized
expiresAt: Date.now() + (tokens.expires_in * 1000)
};
}