--- name: OAuth2 description: Expert guidance for OAuth 2.0 protocol including authorization flows, grant types, token management, OpenID Connect, security best practices, and implementation patterns. Use this when implementing authentication/authorization, working with OAuth providers, securing APIs, or integrating with third-party services. --- # OAuth 2.0 Expert assistance with OAuth 2.0 authorization framework and OpenID Connect. ## Overview OAuth 2.0 is an authorization framework that enables applications to obtain limited access to user accounts. Key concepts: - **Resource Owner**: User who owns the data - **Client**: Application requesting access - **Authorization Server**: Issues access tokens (e.g., Keycloak, Auth0) - **Resource Server**: API that holds protected resources - **Access Token**: Credentials to access protected resources - **Refresh Token**: Credentials to obtain new access tokens ## OAuth 2.0 Flows ### Authorization Code Flow (Most Secure) **Best for**: Server-side web apps with backend **Flow**: ``` 1. Client redirects user to authorization server GET /authorize? response_type=code &client_id=CLIENT_ID &redirect_uri=REDIRECT_URI &scope=read write &state=RANDOM_STATE 2. User authenticates and grants permission 3. Authorization server redirects back with code REDIRECT_URI?code=AUTH_CODE&state=RANDOM_STATE 4. Client exchanges code for tokens POST /token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code &code=AUTH_CODE &redirect_uri=REDIRECT_URI &client_id=CLIENT_ID &client_secret=CLIENT_SECRET 5. Authorization server responds with tokens { "access_token": "ACCESS_TOKEN", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "REFRESH_TOKEN", "scope": "read write" } ``` **Implementation (Node.js)**: ```javascript // Step 1: Redirect to authorization app.get('/login', (req, res) => { const state = crypto.randomBytes(16).toString('hex') req.session.oauthState = state const authUrl = new URL('https://auth.example.com/authorize') authUrl.searchParams.set('response_type', 'code') authUrl.searchParams.set('client_id', CLIENT_ID) authUrl.searchParams.set('redirect_uri', REDIRECT_URI) authUrl.searchParams.set('scope', 'read write') authUrl.searchParams.set('state', state) res.redirect(authUrl.toString()) }) // Step 3 & 4: Handle callback and exchange code app.get('/callback', async (req, res) => { const { code, state } = req.query // Verify state if (state !== req.session.oauthState) { return res.status(400).send('Invalid state') } // Exchange code for token const tokenResponse = await fetch('https://auth.example.com/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: REDIRECT_URI, client_id: CLIENT_ID, client_secret: CLIENT_SECRET, }), }) const tokens = await tokenResponse.json() // Store tokens securely req.session.accessToken = tokens.access_token req.session.refreshToken = tokens.refresh_token res.redirect('/dashboard') }) ``` ### Authorization Code Flow with PKCE **Best for**: Mobile apps, SPAs, any public client **PKCE adds security for public clients that can't keep secrets** **Flow**: ```javascript // Step 1: Generate code verifier and challenge const codeVerifier = base64URLEncode(crypto.randomBytes(32)) const codeChallenge = base64URLEncode( crypto.createHash('sha256').update(codeVerifier).digest() ) // Step 2: Authorization request GET /authorize? response_type=code &client_id=CLIENT_ID &redirect_uri=REDIRECT_URI &scope=read &state=STATE &code_challenge=CODE_CHALLENGE &code_challenge_method=S256 // Step 3: Token request (no client_secret needed) POST /token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code &code=AUTH_CODE &redirect_uri=REDIRECT_URI &client_id=CLIENT_ID &code_verifier=CODE_VERIFIER ``` **Implementation (React)**: ```typescript import { useEffect } from 'react' import { useRouter } from 'next/router' // Generate PKCE challenge function generatePKCE() { const codeVerifier = generateRandomString(128) const codeChallenge = base64URLEncode( sha256(codeVerifier) ) return { codeVerifier, codeChallenge } } function LoginButton() { const router = useRouter() const handleLogin = () => { const { codeVerifier, codeChallenge } = generatePKCE() const state = generateRandomString(32) // Store for later use sessionStorage.setItem('pkce_verifier', codeVerifier) sessionStorage.setItem('oauth_state', state) const authUrl = new URL('https://auth.example.com/authorize') authUrl.searchParams.set('response_type', 'code') authUrl.searchParams.set('client_id', CLIENT_ID) authUrl.searchParams.set('redirect_uri', REDIRECT_URI) authUrl.searchParams.set('scope', 'openid profile email') authUrl.searchParams.set('state', state) authUrl.searchParams.set('code_challenge', codeChallenge) authUrl.searchParams.set('code_challenge_method', 'S256') window.location.href = authUrl.toString() } return } // Callback page function CallbackPage() { const router = useRouter() useEffect(() => { async function handleCallback() { const { code, state } = router.query // Verify state const savedState = sessionStorage.getItem('oauth_state') if (state !== savedState) { throw new Error('Invalid state') } // Get code verifier const codeVerifier = sessionStorage.getItem('pkce_verifier') // Exchange code for token 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', code: code as string, redirect_uri: REDIRECT_URI, client_id: CLIENT_ID, code_verifier: codeVerifier!, }), }) const tokens = await response.json() // Store tokens localStorage.setItem('access_token', tokens.access_token) localStorage.setItem('refresh_token', tokens.refresh_token) // Clean up sessionStorage.removeItem('pkce_verifier') sessionStorage.removeItem('oauth_state') router.push('/dashboard') } handleCallback() }, [router.query]) return
Logging in...
} ``` ### Client Credentials Flow **Best for**: Machine-to-machine, service accounts, server-to-server **Flow**: ```bash # Request token POST /token Content-Type: application/x-www-form-urlencoded grant_type=client_credentials &client_id=CLIENT_ID &client_secret=CLIENT_SECRET &scope=api.read api.write # Response { "access_token": "ACCESS_TOKEN", "token_type": "Bearer", "expires_in": 3600, "scope": "api.read api.write" } ``` **Implementation**: ```javascript async function getServiceToken() { const response = await fetch('https://auth.example.com/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'client_credentials', client_id: process.env.CLIENT_ID, client_secret: process.env.CLIENT_SECRET, scope: 'api.read api.write', }), }) return await response.json() } // Use token async function callProtectedAPI() { const { access_token } = await getServiceToken() const response = await fetch('https://api.example.com/data', { headers: { 'Authorization': `Bearer ${access_token}`, }, }) return await response.json() } ``` ### Resource Owner Password Credentials (Legacy) **⚠️ Not recommended** - Only use when no other flow works ```bash POST /token Content-Type: application/x-www-form-urlencoded grant_type=password &username=USER &password=PASSWORD &client_id=CLIENT_ID &client_secret=CLIENT_SECRET &scope=read ``` ### Implicit Flow (Deprecated) **⚠️ Deprecated** - Use Authorization Code Flow with PKCE instead ## Token Management ### Access Tokens **Characteristics**: - Short-lived (5-15 minutes typical) - Used to access protected resources - Should be treated as opaque strings - Often JWT format but not required **Usage**: ```javascript // Make API request with access token fetch('https://api.example.com/user/profile', { headers: { 'Authorization': `Bearer ${accessToken}`, }, }) ``` ### Refresh Tokens **Purpose**: Obtain new access tokens without re-authentication **Usage**: ```javascript async function refreshAccessToken(refreshToken) { const response = await fetch('https://auth.example.com/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: CLIENT_ID, client_secret: CLIENT_SECRET, }), }) const tokens = await response.json() return tokens } // Automatic token refresh async function apiCall(url, options = {}) { let accessToken = localStorage.getItem('access_token') let response = await fetch(url, { ...options, headers: { ...options.headers, 'Authorization': `Bearer ${accessToken}`, }, }) // Token expired if (response.status === 401) { const refreshToken = localStorage.getItem('refresh_token') const newTokens = await refreshAccessToken(refreshToken) localStorage.setItem('access_token', newTokens.access_token) if (newTokens.refresh_token) { localStorage.setItem('refresh_token', newTokens.refresh_token) } // Retry request with new token response = await fetch(url, { ...options, headers: { ...options.headers, 'Authorization': `Bearer ${newTokens.access_token}`, }, }) } return response } ``` ### Token Storage **Best Practices**: **Browser (SPA)**: - ✅ Memory (most secure, lost on refresh) - ✅ Session storage (cleared on tab close) - ⚠️ Local storage (XSS risk, but convenient) - ❌ Cookies without httpOnly (XSS risk) - ✅ httpOnly cookies (CSRF protection needed) **Server-side**: - ✅ Encrypted session storage - ✅ Secure database with encryption - ❌ Plain text files - ❌ Environment variables (for user tokens) **Implementation**: ```javascript // Secure token storage (Next.js) import { serialize, parse } from 'cookie' // Set token in httpOnly cookie export function setTokenCookie(res, token) { const cookie = serialize('token', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: 3600, path: '/', }) res.setHeader('Set-Cookie', cookie) } // Get token from cookie export function getTokenCookie(req) { const cookies = parse(req.headers.cookie || '') return cookies.token } ``` ## OpenID Connect (OIDC) OAuth 2.0 extension for authentication ### ID Token **JWT containing user information**: ```json { "iss": "https://auth.example.com", "sub": "user-123", "aud": "client-id", "exp": 1234567890, "iat": 1234567890, "name": "John Doe", "email": "john@example.com", "email_verified": true, "picture": "https://example.com/photo.jpg" } ``` ### OIDC Flow ```javascript // Authorization request with openid scope GET /authorize? response_type=code &client_id=CLIENT_ID &redirect_uri=REDIRECT_URI &scope=openid profile email &state=STATE // Token response includes ID token { "access_token": "ACCESS_TOKEN", "id_token": "ID_TOKEN_JWT", "refresh_token": "REFRESH_TOKEN", "token_type": "Bearer", "expires_in": 3600 } // Validate ID token import jwt from 'jsonwebtoken' import jwksClient from 'jwks-rsa' const client = jwksClient({ jwksUri: 'https://auth.example.com/.well-known/jwks.json' }) function getKey(header, callback) { client.getSigningKey(header.kid, (err, key) => { const signingKey = key.publicKey || key.rsaPublicKey callback(null, signingKey) }) } jwt.verify(idToken, getKey, { audience: CLIENT_ID, issuer: 'https://auth.example.com', algorithms: ['RS256'] }, (err, decoded) => { if (err) { console.error('Invalid token') } else { console.log('User:', decoded) } }) ``` ### UserInfo Endpoint ```javascript // Get additional user info async function getUserInfo(accessToken) { const response = await fetch('https://auth.example.com/userinfo', { headers: { 'Authorization': `Bearer ${accessToken}`, }, }) return await response.json() } ``` ### Discovery ```bash # Get provider configuration GET https://auth.example.com/.well-known/openid-configuration # Response includes: { "issuer": "https://auth.example.com", "authorization_endpoint": "https://auth.example.com/authorize", "token_endpoint": "https://auth.example.com/token", "userinfo_endpoint": "https://auth.example.com/userinfo", "jwks_uri": "https://auth.example.com/.well-known/jwks.json", "response_types_supported": ["code", "token", "id_token"], "grant_types_supported": ["authorization_code", "refresh_token"], "token_endpoint_auth_methods_supported": ["client_secret_post"] } ``` ## Scopes ### Standard Scopes - `openid` - Required for OIDC - `profile` - User's profile info (name, picture, etc.) - `email` - User's email address - `address` - User's address - `phone` - User's phone number - `offline_access` - Request refresh token ### Custom Scopes ``` API-specific permissions: - api:read - Read access to API - api:write - Write access to API - api:delete - Delete access to API - admin - Admin access ``` ### Requesting Scopes ```javascript // Request multiple scopes const authUrl = new URL('https://auth.example.com/authorize') authUrl.searchParams.set('scope', 'openid profile email api:read api:write') ``` ## Security Best Practices ### 1. Always Use State Parameter ```javascript // Generate random state const state = crypto.randomBytes(32).toString('hex') sessionStorage.setItem('oauth_state', state) // Include in authorization request authUrl.searchParams.set('state', state) // Verify in callback if (receivedState !== sessionStorage.getItem('oauth_state')) { throw new Error('CSRF attack detected') } ``` ### 2. Use PKCE for Public Clients Always use PKCE for SPAs and mobile apps - no exceptions. ### 3. Validate Tokens ```javascript // Validate JWT - Verify signature using public key - Check issuer (iss) - Check audience (aud) - Check expiration (exp) - Check not before (nbf) ``` ### 4. Short-Lived Access Tokens ``` Access token: 5-15 minutes Refresh token: Days to months (with rotation) ``` ### 5. Token Rotation ```javascript // Refresh token rotation When using refresh token: 1. Issue new access token 2. Issue new refresh token 3. Invalidate old refresh token ``` ### 6. Secure Token Storage ```javascript // ✅ Best practices - httpOnly cookies (server-side) - Encrypted session storage (server-side) - Memory only (SPA, lost on refresh) // ❌ Avoid - Local storage (XSS vulnerable) - Session storage without proper sanitization - URL parameters - Plain text anywhere ``` ### 7. Redirect URI Validation ```javascript // Server must validate exact match Registered: https://app.example.com/callback Valid: https://app.example.com/callback Invalid: https://app.example.com/callback/extra Invalid: https://evil.com/callback ``` ### 8. HTTPS Only All OAuth communication must use HTTPS in production. ### 9. Rate Limiting Implement rate limiting on: - Token endpoint - Authorization endpoint - UserInfo endpoint ### 10. Audit Logging Log all OAuth events: - Authorization requests - Token issuances - Token refreshes - Failed attempts ## Common Patterns ### API Protection ```javascript // Express middleware function requireAuth(req, res, next) { const token = req.headers.authorization?.split(' ')[1] if (!token) { return res.status(401).json({ error: 'No token provided' }) } try { const decoded = jwt.verify(token, PUBLIC_KEY) req.user = decoded next() } catch (error) { return res.status(401).json({ error: 'Invalid token' }) } } // Protected route app.get('/api/protected', requireAuth, (req, res) => { res.json({ data: 'Protected data', user: req.user }) }) // Role-based protection function requireRole(role) { return (req, res, next) => { if (!req.user?.roles?.includes(role)) { return res.status(403).json({ error: 'Insufficient permissions' }) } next() } } app.delete('/api/users/:id', requireAuth, requireRole('admin'), (req, res) => { // Delete user }) ``` ### Silent Authentication ```javascript // Attempt silent authentication in hidden iframe function silentAuthentication() { return new Promise((resolve, reject) => { const iframe = document.createElement('iframe') iframe.style.display = 'none' const authUrl = new URL('https://auth.example.com/authorize') authUrl.searchParams.set('prompt', 'none') authUrl.searchParams.set('response_type', 'code') authUrl.searchParams.set('client_id', CLIENT_ID) authUrl.searchParams.set('redirect_uri', SILENT_REDIRECT_URI) iframe.src = authUrl.toString() window.addEventListener('message', (event) => { if (event.origin !== 'https://app.example.com') return if (event.data.code) { resolve(event.data.code) } else { reject(new Error('Silent auth failed')) } document.body.removeChild(iframe) }) document.body.appendChild(iframe) }) } ``` ## Troubleshooting ### Invalid Grant - Expired authorization code - Code already used - Mismatched redirect URI - Invalid code verifier (PKCE) ### Invalid Client - Wrong client credentials - Client not found - Client disabled ### Unauthorized Client - Client not allowed for grant type - Client not allowed for scope ### Access Denied - User denied permission - Invalid scope requested ## Resources - RFC 6749 (OAuth 2.0): https://tools.ietf.org/html/rfc6749 - RFC 7636 (PKCE): https://tools.ietf.org/html/rfc7636 - OpenID Connect: https://openid.net/connect/ - OAuth 2.0 Security Best Practices: https://tools.ietf.org/html/draft-ietf-oauth-security-topics