Expert guidance for OAuth 2.0 protocol including authorization flows, grant types, token management, OpenID Connect, security best practices, and implementation patterns...
Expert assistance with OAuth 2.0 authorization framework and OpenID Connect.
OAuth 2.0 is an authorization framework that enables applications to obtain limited access to user accounts. Key concepts:
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):
// 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')
})
Best for: Mobile apps, SPAs, any public client
PKCE adds security for public clients that can't keep secrets
Flow:
// 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):
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 <button onClick={handleLogin}>Login</button>
}
// 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 <div>Logging in...</div>
}
Best for: Machine-to-machine, service accounts, server-to-server
Flow:
# 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:
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()
}
⚠️ Not recommended - Only use when no other flow works
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
⚠️ Deprecated - Use Authorization Code Flow with PKCE instead
Characteristics:
Usage:
// Make API request with access token
fetch('https://api.example.com/user/profile', {
headers: {
'Authorization': `Bearer ${accessToken}`,
},
})
Purpose: Obtain new access tokens without re-authentication
Usage:
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
}
Best Practices:
Browser (SPA):
Server-side:
Implementation:
// 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
}
OAuth 2.0 extension for authentication
JWT containing user information:
{
"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"
}
// 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)
}
})
// 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()
}
# 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"]
}
openid - Required for OIDCprofile - User's profile info (name, picture, etc.)email - User's email addressaddress - User's addressphone - User's phone numberoffline_access - Request refresh tokenAPI-specific permissions:
- api:read - Read access to API
- api:write - Write access to API
- api:delete - Delete access to API
- admin - Admin access
// Request multiple scopes
const authUrl = new URL('https://auth.example.com/authorize')
authUrl.searchParams.set('scope', 'openid profile email api:read api:write')
// 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')
}
Always use PKCE for SPAs and mobile apps - no exceptions.
// Validate JWT
- Verify signature using public key
- Check issuer (iss)
- Check audience (aud)
- Check expiration (exp)
- Check not before (nbf)
Access token: 5-15 minutes
Refresh token: Days to months (with rotation)
// Refresh token rotation
When using refresh token:
1. Issue new access token
2. Issue new refresh token
3. Invalidate old refresh token
// ✅ 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
// 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
All OAuth communication must use HTTPS in production.
Implement rate limiting on:
Log all OAuth events:
// 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
})
// 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)
})
}