OAuth2 and OpenID Connect
OAuth2 Flows
Read aloud is unavailable in this browser.
The Authorization Revolution
In the early days of the web, if you wanted a third-party application to access your data from another service, you had to do something terrifying: give that application your password. Photo printing services would ask for your Facebook password. Calendar apps would request your Google credentials. This anti-pattern created massive security vulnerabilities—if any single third-party service was compromised, attackers could access every system you'd shared credentials with.
OAuth 2.0 emerged as the solution to this fundamental problem. It introduced a revolutionary concept: delegated authorization—the ability to grant limited access to your resources without sharing your credentials. Today, OAuth 2.0 is the backbone of modern authorization, powering billions of daily authorizations across Google, Microsoft, GitHub, Twitter, and virtually every major platform.
Understanding OAuth 2.0 flows isn't just academic knowledge—it's essential for any engineer building systems that interact with third-party services or expose APIs to external applications.
By the end of this page, you will understand OAuth 2.0's fundamental architecture, master each grant type's flow and use cases, recognize security considerations for each approach, and be equipped to select the appropriate flow for any authorization scenario.
Understanding OAuth 2.0
OAuth 2.0 is an authorization framework (not an authentication protocol—that distinction matters and we'll address it later). It enables a third-party application to obtain limited access to an HTTP service, either on behalf of a resource owner or by allowing the third-party application to obtain access on its own behalf.
The Core Problem OAuth Solves:
Consider this scenario: You want a photo editing app to access your Google Photos. Without OAuth:
- You'd give the photo app your Google password
- The app could access everything in your Google account
- You couldn't revoke access without changing your password
- If the app was compromised, your entire Google account is exposed
With OAuth 2.0:
- You authenticate directly with Google (never sharing credentials with the app)
- Google asks "Do you want to allow PhotoApp to access your photos?"
- You grant specific, limited permissions
- The app receives a token (not your password)
- You can revoke the app's access at any time
- Even if the app is compromised, attackers only get the limited token, not your credentials
- •Resource Owner — The entity capable of granting access to a protected resource. When the resource owner is a person, it's the end-user who authorizes access.
- •Resource Server — The server hosting the protected resources. It accepts and validates access tokens to serve protected resources. Examples: Google Photos API, GitHub API.
- •Client — The application making protected resource requests on behalf of the resource owner. The 'client' is the third-party app wanting access, not the end-user.
- •Authorization Server — The server issuing access tokens after successfully authenticating the resource owner and obtaining authorization. Often the same entity as the resource server.
OAuth 2.0's genius lies in introducing tokens as intermediaries. Instead of sharing long-lived credentials, the system issues short-lived tokens with specific scopes. Tokens can be revoked, have limited permissions, and don't expose underlying credentials. This paradigm shift fundamentally changed how we think about authorization.
Authorization Code Flow
The Authorization Code Flow is the most secure and commonly used OAuth 2.0 flow for applications with a secure backend server. It's the gold standard for web applications with server-side components and mobile apps using Proof Key for Code Exchange (PKCE).
Why It's the Most Secure:
This flow is designed around a critical security principle: the access token never passes through the user's browser. The browser only sees an authorization code—a short-lived, single-use credential that's exchanged for the real access token via a secure server-to-server call. This prevents token leakage through browser history, logs, or man-in-the-middle attacks on the front channel.
| Step | Actor | Action | Security Consideration |
|---|---|---|---|
| 1 | User | Clicks 'Login with Google' on the client app | User initiates flow explicitly—no silent background authorization |
| 2 | Client | Redirects to Authorization Server with client_id, redirect_uri, scope, state | state parameter prevents CSRF attacks |
| 3 | Auth Server | Authenticates user, displays consent screen | Direct user-to-provider interaction—credentials never touch client |
| 4 | User | Grants consent to requested scopes | User explicitly sees and approves permissions |
| 5 | Auth Server | Redirects back to client with authorization code | Code is short-lived (<10 min), single-use |
| 6 | Client (Backend) | Exchanges code + client_secret for access token | Server-to-server call—token never in browser |
| 7 | Auth Server | Returns access token (and optionally refresh token) | Token delivered over secure back channel |
| 8 | Client | Uses access token to call Resource Server | Token attached to API calls, typically in Authorization header |
123456789101112131415161718192021222324252627282930313233343536373839404142
// Step 1: Construct authorization URLconst authorizationUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');authorizationUrl.searchParams.set('client_id', process.env.GOOGLE_CLIENT_ID);authorizationUrl.searchParams.set('redirect_uri', 'https://myapp.com/callback');authorizationUrl.searchParams.set('response_type', 'code');authorizationUrl.searchParams.set('scope', 'openid email profile');authorizationUrl.searchParams.set('state', generateCryptoRandomState()); // CSRF protectionauthorizationUrl.searchParams.set('access_type', 'offline'); // Request refresh token // Redirect user to authorization URLres.redirect(authorizationUrl.toString()); // Step 2: Handle callback - exchange code for tokensapp.get('/callback', async (req, res) => { const { code, state } = req.query; // Verify state matches what we stored (CSRF protection) if (state !== req.session.oauthState) { return res.status(403).send('Invalid state parameter'); } // Exchange authorization code for tokens (server-to-server) const tokenResponse = await fetch('https://oauth2.googleapis.com/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ code: code as string, client_id: process.env.GOOGLE_CLIENT_ID, client_secret: process.env.GOOGLE_CLIENT_SECRET, // Server-side only! redirect_uri: 'https://myapp.com/callback', grant_type: 'authorization_code', }), }); const tokens = await tokenResponse.json(); // tokens = { access_token, refresh_token, expires_in, token_type, id_token } // Store tokens securely - never expose to client await storeTokensSecurely(req.session.userId, tokens); res.redirect('/dashboard');});The state parameter prevents Cross-Site Request Forgery (CSRF) attacks. Without it, an attacker could trick a user into authorizing the attacker's account to be linked to the victim's session. Always generate a cryptographically random state, store it in the session, and verify it matches on callback.
Authorization Code Flow with PKCE
Proof Key for Code Exchange (PKCE) extends the Authorization Code Flow for public clients—applications that cannot securely store a client secret, such as mobile apps, single-page applications (SPAs), and desktop applications.
The Problem with Public Clients:
Mobile apps and SPAs are distributed to user devices. Any 'secret' embedded in them can be extracted through reverse engineering or browser developer tools. Without a secret, how can the authorization server trust that the entity exchanging the code is the same entity that initiated the flow?
PKCE's Elegant Solution:
PKCE introduces a dynamic, per-request secret:
- Before redirecting, the client generates a random code verifier (a high-entropy string)
- The client computes a code challenge (SHA-256 hash of the verifier, base64-encoded)
- The code challenge is sent with the authorization request
- When exchanging the code, the client sends the original code verifier
- The authorization server verifies that SHA-256(verifier) matches the stored challenge
This ensures that only the entity that initiated the request can complete the token exchange, even without a static secret.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
// PKCE Implementation for SPAs/Mobile Appsimport * as crypto from 'crypto'; // Generate cryptographically random code verifier (43-128 characters)function generateCodeVerifier(): string { const buffer = crypto.randomBytes(32); return base64URLEncode(buffer);} // Generate code challenge from verifier using SHA-256async function generateCodeChallenge(verifier: string): Promise<string> { const encoder = new TextEncoder(); const data = encoder.encode(verifier); const digest = await crypto.subtle.digest('SHA-256', data); return base64URLEncode(new Uint8Array(digest));} // Base64 URL encoding (no padding, URL-safe characters)function base64URLEncode(buffer: Uint8Array): string { return btoa(String.fromCharCode(...buffer)) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, '');} // Step 1: Initiate PKCE flowasync function initiateOAuth() { const codeVerifier = generateCodeVerifier(); const codeChallenge = await generateCodeChallenge(codeVerifier); // Store verifier securely (will need it later) sessionStorage.setItem('pkce_code_verifier', codeVerifier); const authUrl = new URL('https://auth.example.com/authorize'); authUrl.searchParams.set('client_id', 'spa-client-id'); authUrl.searchParams.set('redirect_uri', 'https://spa.example.com/callback'); authUrl.searchParams.set('response_type', 'code'); authUrl.searchParams.set('scope', 'openid profile email'); authUrl.searchParams.set('state', generateSecureState()); authUrl.searchParams.set('code_challenge', codeChallenge); authUrl.searchParams.set('code_challenge_method', 'S256'); // SHA-256 window.location.href = authUrl.toString();} // Step 2: Exchange code with verifier (no client_secret needed!)async function handleCallback(code: string) { const codeVerifier = sessionStorage.getItem('pkce_code_verifier'); sessionStorage.removeItem('pkce_code_verifier'); const response = await fetch('https://auth.example.com/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', client_id: 'spa-client-id', code: code, redirect_uri: 'https://spa.example.com/callback', code_verifier: codeVerifier!, // Proves we initiated the flow }), }); return response.json();}While PKCE was designed for public clients, OAuth 2.1 (the upcoming revision) recommends PKCE for ALL clients, including confidential ones with secrets. It adds defense-in-depth and protects against authorization code injection attacks even when secrets are used.
Client Credentials Flow
The Client Credentials Flow is used when the client application itself needs to access resources—not on behalf of a user. This is the machine-to-machine (M2M) flow, essential for backend services, daemons, microservices, and automated processes.
Key Distinction:
In other flows, a user (Resource Owner) authorizes access. In Client Credentials, the application is the Resource Owner. There's no user involvement, no consent screen, no interactive authorization—just one service authenticating to another.
Common Use Cases:
- Microservice A calls Microservice B's API
- A cron job accesses a protected data export endpoint
- A CI/CD pipeline deploys to an authenticated service
- Backend services accessing third-party APIs (Stripe, Twilio, AWS)
- •Service-to-service API calls
- •Automated background processes
- •CLI tools with machine identities
- •Internal microservice authorization
- •Pre-authorized batch operations
- •Any scenario requiring user consent
- •Accessing user-specific resources
- •Mobile or browser applications
- •When user context is needed
- •Multi-tenant user-scoped access
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
// Client Credentials Flow - Machine-to-Machineinterface TokenResponse { access_token: string; token_type: string; expires_in: number; scope?: string;} class ServiceClient { private accessToken: string | null = null; private tokenExpiry: Date | null = null; constructor( private clientId: string, private clientSecret: string, private tokenEndpoint: string, private scope: string, ) {} // Obtain access token using client credentials async getAccessToken(): Promise<string> { // Return cached token if still valid if (this.accessToken && this.tokenExpiry && new Date() < this.tokenExpiry) { return this.accessToken; } // Request new token const response = await fetch(this.tokenEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', // Many servers support HTTP Basic Auth for client credentials 'Authorization': `Basic ${Buffer.from( `${this.clientId}:${this.clientSecret}` ).toString('base64')}`, }, body: new URLSearchParams({ grant_type: 'client_credentials', scope: this.scope, }), }); if (!response.ok) { throw new Error(`Token request failed: ${response.status}`); } const data: TokenResponse = await response.json(); // Cache token with buffer time (refresh 60s before expiry) this.accessToken = data.access_token; this.tokenExpiry = new Date(Date.now() + (data.expires_in - 60) * 1000); return this.accessToken; } // Make authenticated API call async callApi(endpoint: string, options: RequestInit = {}): Promise<Response> { const token = await this.getAccessToken(); return fetch(endpoint, { ...options, headers: { ...options.headers, 'Authorization': `Bearer ${token}`, }, }); }} // Usageconst paymentService = new ServiceClient( process.env.PAYMENT_SERVICE_CLIENT_ID!, process.env.PAYMENT_SERVICE_CLIENT_SECRET!, 'https://auth.internal.example.com/oauth/token', 'payment:read payment:write',); // Calls are automatically authenticatedconst transactions = await paymentService.callApi( 'https://payment.internal.example.com/api/transactions');Client credentials (client_id + client_secret) are essentially the service's username and password. They must be stored securely using environment variables, secrets managers (AWS Secrets Manager, HashiCorp Vault), or cloud provider secret storage. Never commit secrets to version control or embed them in container images.
Implicit Flow (Legacy/Deprecated)
The Implicit Flow was designed for browser-based JavaScript applications before CORS was widely supported. It returned access tokens directly in the URL fragment, skipping the code exchange step. This flow is now deprecated and should NOT be used for new implementations.
Why It Was Created:
In the early days of OAuth 2.0 (circa 2012), browsers had severe limitations:
- CORS (Cross-Origin Resource Sharing) wasn't universally supported
- SPAs couldn't make cross-origin POST requests to token endpoints
- The only option was returning tokens via redirect
Why It's Deprecated:
The Implicit Flow has fundamental security weaknesses:
- Tokens in URL fragments: These can leak via browser history, referrer headers, and logs
- No client authentication: Any entity receiving the redirect can capture the token
- No refresh tokens: Tokens must be short-lived, requiring frequent re-authentication
- Vulnerable to token injection: Attackers can inject stolen tokens into legitimate sessions
OAuth 2.0 Security Best Current Practice (RFC 9700) explicitly recommends against using the Implicit Flow. All major identity providers (Google, Microsoft, Auth0, Okta) now recommend Authorization Code Flow with PKCE for SPAs instead. If you encounter legacy systems using Implicit Flow, plan a migration to PKCE.
| Aspect | Implicit Flow | Auth Code + PKCE |
|---|---|---|
| Token Exposure | Token in URL fragment—high exposure risk | Token returned via secure POST—never in URL |
| Refresh Tokens | Not supported—constant re-authentication | Fully supported—silent token refresh |
| Client Auth | None possible—tokens can be intercepted | PKCE proves client identity dynamically |
| Token Lifetime | Must be very short (<1 hour) | Can be longer with refresh token rotation |
| Security Status | Deprecated, known vulnerabilities | Current best practice, actively maintained |
Migration Path:
If you have an SPA using Implicit Flow:
- Update your authorization library (most now support PKCE by default)
- Change
response_type=tokentoresponse_type=code - Implement PKCE code verifier/challenge generation
- Set up token exchange endpoint handling
- Update your identity provider configuration to enable PKCE
- Implement secure token storage (in-memory or httpOnly cookies via backend)
Modern libraries like @auth0/auth0-spa-js, oidc-client-ts, and @azure/msal-browser handle PKCE automatically.
Resource Owner Password Credentials Flow
The Resource Owner Password Credentials (ROPC) Flow allows the client to directly collect the user's username and password and exchange them for tokens. This flow exists for very specific legacy migration scenarios and should be avoided in new designs.
When It Might Be Acceptable:
- Migrating from legacy systems that directly handled credentials
- First-party applications where the app and auth server are the same trust domain
- CLI tools for first-party services (still, device flow is often better)
- Enterprise scenarios with controlled, trusted environments
Why It's Problematic:
- Credentials exposure: The client application sees the user's password
- Phishing vulnerability: Users are trained to enter credentials in third-party apps
- No MFA support: Password-only authentication undermines modern security
- Breaks federation: SSO and social logins don't work with ROPC
- Trust assumption: Assumes the client is completely trusted—rarely true
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
// ROPC Flow - ONLY for legacy migration scenarios// ⚠️ NOT RECOMMENDED for new implementations async function legacyPasswordLogin(username: string, password: string) { // WARNING: User credentials are directly handled by the client // This creates security and trust concerns const response = await fetch('https://auth.example.com/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'password', username: username, password: password, client_id: 'legacy-app-client-id', client_secret: 'legacy-app-client-secret', // If confidential client scope: 'openid profile email', }), }); if (!response.ok) { const error = await response.json(); throw new Error(`Authentication failed: ${error.error_description}`); } return response.json(); // { access_token, refresh_token, expires_in, ... }} // Migration example: Wrapping legacy login with OAuthclass MigrationAuthService { // Phase 1: Use ROPC during migration async legacyLogin(username: string, password: string) { return legacyPasswordLogin(username, password); } // Phase 2: Offer modern OAuth flow as alternative async modernLogin() { // Redirect to Authorization Code + PKCE flow window.location.href = buildPKCEAuthUrl(); } // Phase 3: Deprecate legacy, use modern flow only async login() { return this.modernLogin(); }}The upcoming OAuth 2.1 specification completely removes the Resource Owner Password Credentials grant type. If you're designing new systems or modernizing existing ones, avoid ROPC entirely. Use Authorization Code + PKCE for interactive scenarios or Device Flow for input-constrained devices.
Device Authorization Flow
The Device Authorization Flow (also called Device Code Flow) is designed for devices with limited input capabilities—smart TVs, game consoles, IoT devices, and CLI tools. These devices either lack browsers or have input methods that make typing credentials painful.
The User Experience:
- Device displays: "Visit https://auth.example.com/device and enter code: WHDJ-QFXZ"
- User opens URL on their phone/laptop (separate device with proper input)
- User enters the code and authenticates normally
- Device automatically detects authorization and receives tokens
Why This Flow is Brilliant:
- Authentication happens on a device the user trusts (their phone/laptop)
- Full security features available (MFA, biometrics, password managers)
- The constrained device never sees credentials
- Works even for devices without displays (read code over speakers)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
// Device Authorization Flow Implementationinterface DeviceCodeResponse { device_code: string; user_code: string; verification_uri: string; verification_uri_complete?: string; // URI with code pre-filled expires_in: number; interval: number; // Polling interval in seconds} class DeviceAuth { constructor( private clientId: string, private authServer: string, private scope: string, ) {} // Step 1: Request device and user codes async initiateDeviceAuthorization(): Promise<DeviceCodeResponse> { const response = await fetch(`${this.authServer}/device/code`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ client_id: this.clientId, scope: this.scope, }), }); return response.json(); } // Step 2: Display to user displayUserInstructions(deviceCode: DeviceCodeResponse): void { console.log('' + '='.repeat(60)); console.log('To sign in, visit: ' + deviceCode.verification_uri); console.log('And enter code: ' + deviceCode.user_code); console.log('='.repeat(60) + ''); // If QR code support exists, display it if (deviceCode.verification_uri_complete) { generateQRCode(deviceCode.verification_uri_complete); } } // Step 3: Poll for authorization async pollForToken(deviceCode: string, interval: number, timeout: number) { const startTime = Date.now(); while (Date.now() - startTime < timeout * 1000) { await sleep(interval * 1000); const response = await fetch(`${this.authServer}/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'urn:ietf:params:oauth:grant-type:device_code', device_code: deviceCode, client_id: this.clientId, }), }); const data = await response.json(); if (data.access_token) { return data; // Authorization successful! } // Handle pending states switch (data.error) { case 'authorization_pending': // User hasn't authorized yet, keep polling continue; case 'slow_down': // Server says slow down, increase interval interval += 5; continue; case 'access_denied': throw new Error('User denied authorization'); case 'expired_token': throw new Error('Device code expired, restart flow'); default: throw new Error(`Unknown error: ${data.error}`); } } throw new Error('Authorization timeout'); } // Full flow async authorize() { const deviceCode = await this.initiateDeviceAuthorization(); this.displayUserInstructions(deviceCode); return this.pollForToken( deviceCode.device_code, deviceCode.interval, deviceCode.expires_in, ); }} // CLI Usageconst auth = new DeviceAuth( 'cli-tool-client-id', 'https://auth.example.com/oauth', 'openid profile email offline_access',); const tokens = await auth.authorize();console.log('Successfully authenticated!');Device Flow is used by YouTube on Smart TVs, Netflix on game consoles, GitHub CLI, Azure CLI, Google Cloud CLI, and AWS SSO. Whenever you see 'sign in on another device,' it's likely Device Authorization Flow in action.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.