What is OAuth 2.0?
OAuth 2.0 is an authorization framework that allows third-party applications to access user resources without exposing passwords.
Analogy: Instead of giving your hotel master key to the valet, you give them a valet key that only opens the parking garage.
The Problem: Password Sharing
Bad old way: User → Gives Gmail password to Spotify Spotify → Logs into Gmail as user Result: Spotify has FULL access to Gmail forever OAuth 2.0 way: User → Authorizes Spotify to "Read Contacts" only Google → Gives Spotify temporary token for contacts Result: Spotify has LIMITED access, user can revoke anytime
Key Concepts
Roles
- Resource Owner: You (the user with data)
- Client: Third-party app (e.g., Spotify)
- Authorization Server: Issues tokens (e.g., accounts.google.com)
- Resource Server: Hosts protected data (e.g., Gmail API)
Tokens
| Token Type | Purpose | Lifetime | Format |
|---|---|---|---|
| Access Token | API access | 1 hour | Opaque string or JWT |
| Refresh Token | Get new access token | Days/months | Opaque string |
| ID Token (OIDC) | User identity | N/A (validate once) | JWT |
OAuth 2.0 Flows
1. Authorization Code Flow (Most Secure)
For server-side web apps with backend.
sequenceDiagram
participant User
participant Client as Client App
participant AuthServer as Authorization Server
participant API as Resource Server
User->>Client: Click "Sign in with Google"
Client->>AuthServer: Redirect to /authorize
AuthServer->>User: Show consent screen
User->>AuthServer: Approve
AuthServer->>Client: Redirect with code=ABC123
Client->>AuthServer: POST /token (code + client_secret)
AuthServer->>Client: access_token, refresh_token
Client->>API: GET /user/profile (Bearer token)
API->>Client: User data
Step-by-step:
// Step 1: Redirect user to authorization server
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?` +
`client_id=${CLIENT_ID}&` +
`redirect_uri=${REDIRECT_URI}&` +
`response_type=code&` +
`scope=openid email profile&` +
`state=random_string_to_prevent_csrf`;
res.redirect(authUrl);
// Step 2: User approves, Google redirects back with code
// GET /callback?code=ABC123&state=random_string_to_prevent_csrf
// Step 3: Exchange code for tokens (server-side)
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
code: code,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET, // SECRET! Never expose
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code'
})
});
const { access_token, refresh_token, id_token } = await tokenResponse.json();
// Step 4: Use access token to call API
const userInfo = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: { 'Authorization': `Bearer ${access_token}` }
});
Security: Client secret never exposed to browser
2. Implicit Flow (Deprecated)
⚠️ Deprecated: Don't use this. Use Authorization Code + PKCE instead.
Old browser-only flow: User approves → Redirect with access_token in URL fragment Problem: Token exposed in browser history
3. Authorization Code + PKCE (For SPAs & Mobile)
PKCE (Proof Key for Code Exchange) makes OAuth secure for public clients (no client secret).
// Step 1: Generate code verifier and challenge
function generateCodeVerifier() {
return base64urlencode(crypto.randomBytes(32));
}
function generateCodeChallenge(verifier) {
return base64urlencode(sha256(verifier));
}
const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);
// Store verifier in sessionStorage
sessionStorage.setItem('pkce_verifier', codeVerifier);
// Step 2: Redirect to authorization with challenge
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?` +
`client_id=${CLIENT_ID}&` +
`redirect_uri=${REDIRECT_URI}&` +
`response_type=code&` +
`scope=openid email&` +
`code_challenge=${codeChallenge}&` +
`code_challenge_method=S256`;
window.location.href = authUrl;
// Step 3: Exchange code for token with verifier
const verifier = sessionStorage.getItem('pkce_verifier');
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
body: new URLSearchParams({
code: code,
client_id: CLIENT_ID,
// NO client_secret needed!
code_verifier: verifier,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code'
})
});
How PKCE prevents attacks:
Attacker intercepts authorization code Attacker tries to exchange code Server: "Give me code_verifier" Attacker: "I don't have it" ❌ Legitimate client: "Here's the verifier" ✅
4. Client Credentials Flow (Machine-to-Machine)
For backend services with no user.
// Service-to-service authentication
const tokenResponse = await fetch('https://oauth.example.com/token', {
method: 'POST',
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: SERVICE_ID,
client_secret: SERVICE_SECRET,
scope: 'api.read api.write'
})
});
const { access_token } = await tokenResponse.json();
// Use token to call API
await fetch('https://api.example.com/data', {
headers: { 'Authorization': `Bearer ${access_token}` }
});
Use case: Microservice A calling Microservice B
5. Refresh Token Flow
Get new access token without re-authentication.
// Access token expired (401 Unauthorized)
// Use refresh token to get new access token
const refreshResponse = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: STORED_REFRESH_TOKEN,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET
})
});
const { access_token, refresh_token } = await refreshResponse.json();
// Store new tokens
if (refresh_token) {
// Google rotates refresh tokens
updateStoredRefreshToken(refresh_token);
}
OpenID Connect (OIDC)
OIDC = OAuth 2.0 + Authentication
It adds an ID Token (JWT) containing user identity.
ID Token Structure
// ID Token (JWT)
{
"iss": "https://accounts.google.com", // Issuer
"sub": "110169484474386276334", // Subject (user ID)
"aud": "your-client-id.apps.googleusercontent.com", // Audience
"exp": 1638360720, // Expiration
"iat": 1638357120, // Issued at
"email": "user@example.com",
"email_verified": true,
"name": "John Doe",
"picture": "https://..."
}
Validating ID Token
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
// Get Google's public keys
const client = jwksClient({
jwksUri: 'https://www.googleapis.com/oauth2/v3/certs'
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
callback(null, key.publicKey || key.rsaPublicKey);
});
}
// Verify ID token
jwt.verify(idToken, getKey, {
audience: CLIENT_ID,
issuer: 'https://accounts.google.com',
algorithms: ['RS256']
}, (err, decoded) => {
if (err) {
console.error('Invalid token:', err);
} else {
console.log('User:', decoded);
}
});
Critical validations:
- Signature: Verify with Google's public key
- Audience (
aud): Must match your client_id - Issuer (
iss): Must be accounts.google.com - Expiration (
exp): Must be in future
Security Best Practices
1. Token Storage
// ❌ BAD: LocalStorage (vulnerable to XSS)
localStorage.setItem('access_token', token);
// ✅ GOOD: HttpOnly Cookie (not accessible to JavaScript)
res.cookie('access_token', token, {
httpOnly: true,
secure: true, // HTTPS only
sameSite: 'strict', // CSRF protection
maxAge: 3600000 // 1 hour
});
2. State Parameter (CSRF Protection)
// Generate random state
const state = crypto.randomBytes(16).toString('hex');
sessionStorage.setItem('oauth_state', state);
// Include in URL
const authUrl = `...&state=${state}`;
// Verify on callback
const returnedState = req.query.state;
const storedState = sessionStorage.getItem('oauth_state');
if (returnedState !== storedState) {
throw new Error('CSRF attack detected!');
}
3. Scope Limitation
// ❌ BAD: Request all scopes scope: 'https://www.googleapis.com/auth/gmail.modify' // ✅ GOOD: Request minimum needed scope: 'https://www.googleapis.com/auth/gmail.readonly'
4. Token Rotation
// Rotate refresh tokens on use
const { access_token, refresh_token } = await refreshTokens();
if (refresh_token) {
// New refresh token issued, invalidate old one
await db.updateRefreshToken(userId, refresh_token);
}
Real-World Implementations
Express.js Backend
const express = require('express');
const session = require('express-session');
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const app = express();
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { secure: true, httpOnly: true }
}));
app.use(passport.initialize());
app.use(passport.session());
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: "https://example.com/auth/google/callback"
},
(accessToken, refreshToken, profile, done) => {
// Save tokens and user to database
User.findOrCreate({ googleId: profile.id }, {
accessToken,
refreshToken,
profile
}, (err, user) => {
return done(err, user);
});
}
));
app.get('/auth/google',
passport.authenticate('google', { scope: ['profile', 'email'] })
);
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => {
res.redirect('/dashboard');
}
);
React SPA with PKCE
// useAuth.js
import { useEffect, useState } from 'react';
export function useAuth() {
const [user, setUser] = useState(null);
async function login() {
// Generate PKCE challenge
const verifier = generateCodeVerifier();
const challenge = await generateCodeChallenge(verifier);
sessionStorage.setItem('pkce_verifier', verifier);
sessionStorage.setItem('oauth_state', generateState());
// Redirect to OAuth provider
const params = new URLSearchParams({
client_id: process.env.REACT_APP_CLIENT_ID,
redirect_uri: window.location.origin + '/callback',
response_type: 'code',
scope: 'openid profile email',
code_challenge: challenge,
code_challenge_method: 'S256',
state: sessionStorage.getItem('oauth_state')
});
window.location.href = `https://oauth.provider.com/authorize?${params}`;
}
async function handleCallback() {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
// Verify state
if (state !== sessionStorage.getItem('oauth_state')) {
throw new Error('Invalid state');
}
// Exchange code for tokens
const verifier = sessionStorage.getItem('pkce_verifier');
const response = await fetch('/api/auth/token', {
method: 'POST',
body: JSON.stringify({ code, verifier }),
headers: { 'Content-Type': 'application/json' }
});
const { user } = await response.json();
setUser(user);
}
return { user, login, handleCallback };
}
Common Providers
Authorization URL: https://accounts.google.com/o/oauth2/v2/auth Token URL: https://oauth2.googleapis.com/token UserInfo URL: https://www.googleapis.com/oauth2/v2/userinfo JWKS URL: https://www.googleapis.com/oauth2/v3/certs
GitHub
Authorization URL: https://github.com/login/oauth/authorize Token URL: https://github.com/login/oauth/access_token User URL: https://api.github.com/user
Auth0 (Enterprise)
Authorization URL: https://{tenant}.auth0.com/authorize
Token URL: https://{tenant}.auth0.com/oauth/token
UserInfo URL: https://{tenant}.auth0.com/userinfo
Interview Tips 💡
When discussing OAuth in system design interviews:
- Explain the problem: "Users shouldn't give third-party apps their passwords..."
- Choose right flow: "For web app with backend, use Authorization Code Flow. For SPA, use PKCE..."
- Security concerns: "Store tokens in HttpOnly cookies, not localStorage. Always validate state parameter..."
- Token management: "Access tokens expire in 1 hour, use refresh tokens to get new ones..."
- Scope limitation: "Request minimum scopes needed, follow principle of least privilege..."
- Real examples: "Google 'Sign in with Google' uses OAuth 2.0 + OIDC..."
Related Concepts
- JWT (JSON Web Tokens) — Token format used in OIDC
- Session Management — Alternative to token-based auth
- API Gateway — Centralized OAuth enforcement
- CORS — Cross-origin requests in OAuth flows
- CSRF Protection — Important for OAuth callbacks
About ScaleWiki
ScaleWiki is an interactive educational platform dedicated to demystifying distributed systems, software architecture, and system design. Our mission is to provide high-quality, technically accurate resources for software engineers preparing for interviews or solving complex scaling challenges in production.
Read more about our Editorial Guidelines & Authorship.
Educational Disclaimer: The architectural patterns and system designs discussed in this article are based on common industry practices, technical whitepapers, and public engineering blogs. Actual implementations in enterprise environments may vary significantly based on specific product requirements, legacy constraints, and evolving technologies.
Related Articles
DNS Architecture
The phonebook of the internet. How Domain Name System works, the hierarchy of Route 53, and recursive vs iterative resolution strategies.
HTTP Evolution (H1 to H3)
From text-based HTTP/1.1 to binary HTTP/2 and UDP-based HTTP/3 (QUIC). Why we needed upgrades and how they solve Head-of-Line Blocking.
Proxies: Forward vs Reverse
Understanding proxy servers that act as intermediaries between clients and servers, including forward proxies for client anonymity and reverse proxies for load balancing, security, and caching.