Introduction to OAuth 2.0
OAuth 2.0 is an industry-standard protocol for authorization that enables third-party applications to obtain limited access to a user's account on a server without exposing the user's credentials. In simpler terms, it allows users to share specific data with applications while keeping their usernames and passwords secret.
Real-World Analogy
Think of OAuth 2.0 as a valet key for your car. A valet key allows a parking attendant to park your car but prevents access to your trunk and glovebox. Similarly, OAuth 2.0 lets applications access specific parts of your account without giving them your full credentials.
For example, when you click "Sign in with Google" on a website:
- You're not sharing your Google password with that website
- You're granting the website specific permissions (like reading your profile info)
- You can revoke this access later without changing your Google password
- The website gets a secure "token" that represents your permissions
Core Concepts and Terminology
| Term | Description | Example |
|---|---|---|
| Resource Owner | The user who owns the data/resource | You, when giving a site access to your Google account |
| Client | The application requesting access | A website using "Login with Google" |
| Authorization Server | Verifies identity and issues tokens | Google's OAuth server |
| Resource Server | Server hosting the protected resources | Google's API server holding your profile data |
| Access Token | Credential used to access protected resources | The token the website receives to access your data |
| Refresh Token | Credential to obtain new access tokens | Long-lived token to get new short-lived access tokens |
| Scope | Permissions the client is requesting | profile, email, calendar.readonly |
| Consent | User's permission for the request | The permission dialog shown by Google |
OAuth 2.0 Grant Types
OAuth 2.0 defines several "grant types" or flows for different use cases. Each flow is designed for specific application types and security considerations:
| Grant Type | Use Case | Security Level | Best For |
|---|---|---|---|
| Authorization Code | Web apps with server backend | High | Traditional web applications |
| Authorization Code with PKCE | Mobile/native apps, SPAs | High | Mobile apps, single-page applications |
| Implicit | Client-side apps (deprecated) | Low | Legacy applications (no longer recommended) |
| Resource Owner Password Credentials | Trusted first-party apps | Medium | Legacy/migration scenarios |
| Client Credentials | Service-to-service | Medium-High | Microservices, backend processes |
| Device Code | Input-constrained devices | Medium | Smart TVs, IoT devices |
Choosing the Right Flow
- For web applications with a backend: Use Authorization Code flow
- For mobile apps and SPAs: Use Authorization Code with PKCE
- For service-to-service communication: Use Client Credentials flow
- For smart TVs, printers, or IoT devices: Use Device Code flow
- Never use Implicit flow for new applications
- Avoid Resource Owner Password Credentials unless absolutely necessary
Authorization Code Flow in Detail
The Authorization Code flow is the most commonly used and most secure OAuth 2.0 flow. It's designed for applications that can securely store a client secret (typically server-side applications).
Step-by-Step Explanation
- Authorization Request:
- The flow begins when the user clicks a "Login with X" button
- The client application redirects to the authorization server
- The redirect includes several parameters:
client_id: Application's registered IDredirect_uri: Where to send the coderesponse_type=code: Indicates the auth code flowscope: Requested permissionsstate: Random value for CSRF protection
- User Authentication & Consent:
- Authorization server authenticates the user (login screen)
- Server presents a consent screen showing requested permissions
- User approves or denies the permission request
- Authorization Code Grant:
- Upon approval, server redirects back to the client's
redirect_uri - The redirect includes an authorization code and the original state value
- The code is short-lived (typically ~10 minutes)
- Upon approval, server redirects back to the client's
- Token Exchange:
- The client makes a server-to-server POST request to exchange the code for tokens
- This request includes:
grant_type=authorization_codecode: The authorization code receivedredirect_uri: Must match the original requestclient_id: Application's IDclient_secret: Application's secret
- If valid, the authorization server returns:
access_token: For API accesstoken_type: Usually "Bearer"expires_in: Token lifetime in secondsrefresh_token: (Optional) For getting new access tokens
- Resource Access:
- The client can now use the access token to request protected resources
- Usually added as an HTTP header:
Authorization: Bearer {token} - The resource server validates the token and responds with the requested data
// Example OAuth 2.0 Authorization Code Flow in Express.js
// Step 1: Redirect to authorization endpoint
app.get('/auth/login', (req, res) => {
// Create and store a random state value for CSRF protection
const state = crypto.randomBytes(16).toString('hex');
req.session.oauthState = state;
// Construct the authorization URL
const authorizationUrl = new URL('https://authorization-server.com/oauth/authorize');
authorizationUrl.searchParams.append('response_type', 'code');
authorizationUrl.searchParams.append('client_id', process.env.OAUTH_CLIENT_ID);
authorizationUrl.searchParams.append('redirect_uri', process.env.OAUTH_REDIRECT_URI);
authorizationUrl.searchParams.append('scope', 'profile email');
authorizationUrl.searchParams.append('state', state);
// Redirect the user
res.redirect(authorizationUrl.toString());
});
// Step 3: Handle the callback with authorization code
app.get('/auth/callback', async (req, res) => {
// Extract the code and state from the query parameters
const { code, state } = req.query;
// Verify the state matches (CSRF protection)
if (!state || state !== req.session.oauthState) {
return res.status(403).send('Invalid state parameter');
}
// Clear the stored state
delete req.session.oauthState;
try {
// Step 4: Exchange code for tokens
const tokenResponse = await axios.post('https://authorization-server.com/oauth/token', {
grant_type: 'authorization_code',
code,
redirect_uri: process.env.OAUTH_REDIRECT_URI,
client_id: process.env.OAUTH_CLIENT_ID,
client_secret: process.env.OAUTH_CLIENT_SECRET
});
// Extract tokens from the response
const { access_token, refresh_token, expires_in } = tokenResponse.data;
// Store tokens securely (e.g., in session or database)
req.session.accessToken = access_token;
// Store refresh token in a more persistent storage
await saveRefreshToken(req.user.id, refresh_token);
// Step 5: Use the access token to get user information
const userInfoResponse = await axios.get('https://api.example.com/userinfo', {
headers: {
'Authorization': `Bearer ${access_token}`
}
});
// Process user information
const userInfo = userInfoResponse.data;
// Create or update user in your database
const user = await findOrCreateUser(userInfo);
// Log the user in
req.session.userId = user.id;
// Redirect to the application
res.redirect('/dashboard');
} catch (error) {
console.error('OAuth Error:', error);
res.status(500).send('Authentication failed');
}
});
Security Considerations for Authorization Code Flow
- Always validate the state parameter to prevent CSRF attacks
- Keep the client secret secure - never expose it in client-side code
- Validate redirect URIs - only redirect to pre-registered URIs
- Use HTTPS for all OAuth requests and redirects
- Store tokens securely - access tokens in short-term storage (session), refresh tokens in secure long-term storage
- Implement proper error handling for all OAuth-related requests
Authorization Code Flow with PKCE
Proof Key for Code Exchange (PKCE, pronounced "pixie") is an extension to the Authorization Code flow that provides additional security for public clients, such as mobile apps and single-page applications, which cannot securely store a client secret.
Key PKCE Concepts
- Code Verifier: A high-entropy random string generated by the client
- Code Challenge: A transformed version of the code verifier that is sent in the authorization request
- Code Challenge Method: The method used to transform the verifier into the challenge (S256 is recommended)
PKCE Flow Steps
- Code Verifier Generation:
- Client generates a cryptographically random code verifier (43-128 characters)
- Client creates a code challenge by hashing the verifier (SHA-256) and base64-URL-encoding it
- Authorization Request:
- The client includes additional parameters:
code_challenge: The transformed verifiercode_challenge_method=S256: The transformation method
- The client includes additional parameters:
- Token Exchange:
- When exchanging the authorization code for tokens, the client includes:
code_verifier: The original verifier value
- The authorization server transforms the verifier using the specified method and validates that it matches the original challenge
- When exchanging the authorization code for tokens, the client includes:
// Example of PKCE implementation in JavaScript
// 1. Generate a random code verifier
function generateCodeVerifier() {
const array = new Uint8Array(32); // 32 bytes = 256 bits
window.crypto.getRandomValues(array);
return base64UrlEncode(array);
}
// Base64Url encoding function
function base64UrlEncode(buffer) {
return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
// 2. Create a code challenge from the verifier
async function generateCodeChallenge(codeVerifier) {
// Hash the verifier with SHA-256
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const digest = await window.crypto.subtle.digest('SHA-256', data);
// Base64Url encode the hash
return base64UrlEncode(digest);
}
// 3. Start the OAuth flow with PKCE
async function startOAuthFlow() {
// Generate and store the code verifier
const codeVerifier = generateCodeVerifier();
localStorage.setItem('pkce_code_verifier', codeVerifier);
// Generate the code challenge
const codeChallenge = await generateCodeChallenge(codeVerifier);
// Generate random state
const state = generateRandomString(16);
localStorage.setItem('pkce_state', state);
// Construct the authorization URL
const authUrl = new URL('https://authorization-server.com/oauth/authorize');
authUrl.searchParams.append('response_type', 'code');
authUrl.searchParams.append('client_id', 'YOUR_CLIENT_ID');
authUrl.searchParams.append('redirect_uri', 'https://your-app.com/callback');
authUrl.searchParams.append('scope', 'profile email');
authUrl.searchParams.append('state', state);
authUrl.searchParams.append('code_challenge', codeChallenge);
authUrl.searchParams.append('code_challenge_method', 'S256');
// Redirect to the authorization server
window.location = authUrl.toString();
}
// 4. Handle the callback and exchange code for tokens
async function handleCallback() {
// Parse the query parameters
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
const storedState = localStorage.getItem('pkce_state');
// Verify state parameter
if (!state || state !== storedState) {
throw new Error('Invalid state parameter');
}
// Get the stored code verifier
const codeVerifier = localStorage.getItem('pkce_code_verifier');
// Exchange the code for tokens
const tokenResponse = await fetch('https://authorization-server.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: 'https://your-app.com/callback',
client_id: 'YOUR_CLIENT_ID',
code_verifier: codeVerifier
})
});
// Process the token response
const tokens = await tokenResponse.json();
// Store the tokens (securely!)
// ...
// Clean up
localStorage.removeItem('pkce_code_verifier');
localStorage.removeItem('pkce_state');
// Continue with the application flow
// ...
}
When to Use PKCE
PKCE should be used in the following scenarios:
- Mobile Applications: Where a client secret cannot be securely stored
- Single-Page Applications (SPAs): Where all code runs in the browser
- Native Desktop Applications: Where extracting a client secret is possible
- IoT Applications: Where the client secret could be exposed
In fact, using PKCE is now considered a best practice for all OAuth 2.0 clients, even those that can securely store a client secret, as it provides an additional layer of security.
Client Credentials Flow
The Client Credentials flow is used for server-to-server authentication where no user is involved. It's perfect for microservices, backend jobs, and other scenarios where the application itself needs to access resources.
Client Credentials Implementation
// Example Node.js Client Credentials flow using Axios
const axios = require('axios');
const qs = require('querystring');
async function getServiceToken() {
try {
// Step 1: Request an access token
const tokenResponse = await axios.post(
'https://authorization-server.com/oauth/token',
qs.stringify({
grant_type: 'client_credentials',
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
scope: 'read:data write:data'
}),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
// Extract the access token
const { access_token, expires_in } = tokenResponse.data;
// Step 2: Use the token to access resources
const apiResponse = await axios.get(
'https://api.example.com/data',
{
headers: {
'Authorization': `Bearer ${access_token}`
}
}
);
return apiResponse.data;
} catch (error) {
console.error('Error in Client Credentials flow:', error.message);
throw error;
}
}
// Usage in a microservice
async function synchronizeData() {
try {
const data = await getServiceToken();
// Process the data...
console.log(`Synchronized ${data.length} records`);
} catch (error) {
console.error('Synchronization failed:', error);
}
}
// Schedule the job
setInterval(synchronizeData, 3600000); // Run every hour
Client Credentials Use Cases
- Microservice Communication: When one service needs to call another service's API
- Scheduled Jobs: For background processes that need API access
- Data Synchronization: For server-side integrations with other systems
- CI/CD Pipelines: For deployment scripts that need API access
Key difference: This flow doesn't involve a user, so there's no user consent or redirect flow.
Refresh Token Flow
Refresh tokens are an important part of OAuth 2.0 that allows clients to obtain new access tokens without requiring the user to re-authenticate. Access tokens are typically short-lived for security, while refresh tokens have a longer lifetime.
Refresh Token Implementation
// Example refresh token implementation in Express.js
const axios = require('axios');
const { promisify } = require('util');
// Function to refresh an expired token
async function refreshAccessToken(refreshToken) {
try {
const response = await axios.post('https://authorization-server.com/oauth/token', {
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET
});
return {
accessToken: response.data.access_token,
refreshToken: response.data.refresh_token || refreshToken, // Some providers return a new refresh token
expiresIn: response.data.expires_in
};
} catch (error) {
console.error('Error refreshing token:', error.message);
throw new Error('Failed to refresh access token');
}
}
// Middleware to handle API requests with token refresh
async function apiRequest(req, res, next) {
try {
// Get user's tokens from the database or session
const { accessToken, refreshToken, expiresAt } = await getUserTokens(req.user.id);
// Check if the access token is expired
const isExpired = Date.now() >= new Date(expiresAt).getTime();
// If token is expired, refresh it
let currentAccessToken = accessToken;
if (isExpired) {
try {
const tokens = await refreshAccessToken(refreshToken);
// Update tokens in the database
await updateUserTokens(req.user.id, {
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
expiresAt: new Date(Date.now() + tokens.expiresIn * 1000)
});
currentAccessToken = tokens.accessToken;
} catch (refreshError) {
// If refresh fails, user needs to re-authenticate
return res.redirect('/auth/login');
}
}
// Make the API request with the valid token
try {
const apiResponse = await axios.get(req.apiEndpoint, {
headers: {
Authorization: `Bearer ${currentAccessToken}`
}
});
// Return the API response
return res.json(apiResponse.data);
} catch (apiError) {
// If API returns 401 even after refresh, token might be revoked
if (apiError.response && apiError.response.status === 401) {
return res.redirect('/auth/login');
}
// Other API errors
return res.status(apiError.response?.status || 500).json({
error: 'API request failed',
details: apiError.message
});
}
} catch (error) {
next(error);
}
}
// Example usage
app.get('/api/user-profile', async (req, res, next) => {
req.apiEndpoint = 'https://api.example.com/user/profile';
return apiRequest(req, res, next);
});
Refresh Token Best Practices
- Secure Storage: Store refresh tokens securely (e.g., in an encrypted database)
- Token Rotation: Use new refresh tokens when they're provided
- Expire Refresh Tokens: Even refresh tokens should have expiration (e.g., 30-90 days)
- Manage Consent: Allow users to revoke access (invalidating refresh tokens)
- Implement Refresh Logic: Automatically refresh when access tokens expire
- Handle Refresh Failures: Redirect to re-authenticate if refresh fails
OAuth 2.0 Security Best Practices
General Security Considerations
- Always use HTTPS for all OAuth 2.0 endpoints
- Validate all input parameters on both client and server
- Store tokens securely, with appropriate access controls
- Implement proper token validation on your resource servers
- Use the state parameter to prevent CSRF attacks
- Implement secure token storage (HttpOnly cookies, secure storage)
- Use appropriate token expiration times based on risk
- Limit scope requests to only what your application needs
- Validate redirect URLs against a whitelist
- Implement PKCE for all public clients
Common Security Vulnerabilities
| Vulnerability | Description | Mitigation |
|---|---|---|
| CSRF Attacks | Forging authorization requests | Use the state parameter, SameSite cookies |
| Token Leakage | Exposing tokens in logs, URLs, etc. | Only send tokens in headers or POST body |
| Open Redirectors | Using unvalidated redirect_uri parameters | Whitelist valid redirect URIs |
| Authorization Code Theft | Stealing codes from insecure clients | Use PKCE, short code lifetimes, one-time use |
| Token Replay | Stolen tokens being reused | Short token lifetimes, token binding |
| Phishing | Fake authorization servers | Use trusted providers, educate users |
Critical Security Updates
The OAuth 2.0 security landscape has evolved. Some key updates:
- Implicit flow is deprecated - Authorization Code with PKCE is the recommended replacement
- PKCE is recommended for all clients, not just public clients
- Additional security headers like Content-Security-Policy are important
- OAuth 2.1 is being developed to consolidate best practices
Implementing Social Login with OAuth 2.0
Social login (or "Login with X") is one of the most common implementations of OAuth 2.0. Let's look at how to implement it in a Node.js application.
General Implementation Steps
- Register your application with the OAuth provider
- Implement the OAuth flow in your application
- Extract user profile information from the provider's API
- Create or update user records in your application
- Establish a session for the authenticated user
// Example social login implementation with Google OAuth
// 1. Install required packages
// npm install express express-session passport passport-google-oauth20
const express = require('express');
const session = require('express-session');
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const app = express();
// 2. Configure session middleware
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}));
// 3. Initialize Passport
app.use(passport.initialize());
app.use(passport.session());
// 4. Configure Google OAuth Strategy
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: 'http://localhost:3000/auth/google/callback',
scope: ['profile', 'email']
},
function(accessToken, refreshToken, profile, done) {
// Find or create user in your database
findOrCreateUser({
googleId: profile.id,
email: profile.emails[0].value,
name: profile.displayName,
picture: profile.photos[0].value
})
.then(user => done(null, user))
.catch(err => done(err));
}
));
// 5. Serialize/deserialize user for sessions
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser((id, done) => {
findUserById(id)
.then(user => done(null, user))
.catch(err => done(err));
});
// 6. Set up routes for authentication
// Start OAuth flow - redirect to Google
app.get('/auth/google',
passport.authenticate('google', { scope: ['profile', 'email'] })
);
// OAuth callback route
app.get('/auth/google/callback',
passport.authenticate('google', {
failureRedirect: '/login',
successRedirect: '/dashboard'
})
);
// Logout route
app.get('/logout', (req, res) => {
req.logout();
res.redirect('/');
});
// 7. Protected route example
app.get('/dashboard', ensureAuthenticated, (req, res) => {
res.render('dashboard', { user: req.user });
});
// Middleware to check if user is authenticated
function ensureAuthenticated(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
res.redirect('/login');
}
// 8. Database helper functions (implementation depends on your database)
async function findOrCreateUser(profile) {
// Implementation depends on your database
// Example with Mongoose:
let user = await User.findOne({ googleId: profile.googleId });
if (!user) {
user = await User.create({
googleId: profile.googleId,
email: profile.email,
name: profile.name,
picture: profile.picture
});
}
return user;
}
async function findUserById(id) {
// Example with Mongoose:
return await User.findById(id);
}
// Start the server
app.listen(3000, () => {
console.log('Server started on http://localhost:3000');
});
Multi-Provider Social Login
// Adding multiple OAuth providers
// 1. Install additional strategies
// npm install passport-facebook passport-github2 passport-twitter
const FacebookStrategy = require('passport-facebook').Strategy;
const GitHubStrategy = require('passport-github2').Strategy;
const TwitterStrategy = require('passport-twitter').Strategy;
// 2. Configure Facebook Strategy
passport.use(new FacebookStrategy({
clientID: process.env.FACEBOOK_APP_ID,
clientSecret: process.env.FACEBOOK_APP_SECRET,
callbackURL: 'http://localhost:3000/auth/facebook/callback',
profileFields: ['id', 'emails', 'name', 'picture.type(large)']
},
function(accessToken, refreshToken, profile, done) {
findOrCreateUser({
facebookId: profile.id,
email: profile.emails?.[0]?.value,
name: `${profile.name.givenName} ${profile.name.familyName}`,
picture: profile.photos?.[0]?.value
})
.then(user => done(null, user))
.catch(err => done(err));
}
));
// 3. Configure GitHub Strategy
passport.use(new GitHubStrategy({
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
callbackURL: 'http://localhost:3000/auth/github/callback'
},
function(accessToken, refreshToken, profile, done) {
findOrCreateUser({
githubId: profile.id,
email: profile.emails?.[0]?.value,
name: profile.displayName || profile.username,
picture: profile.photos?.[0]?.value
})
.then(user => done(null, user))
.catch(err => done(err));
}
));
// 4. Add routes for each provider
app.get('/auth/facebook',
passport.authenticate('facebook', { scope: ['email'] })
);
app.get('/auth/facebook/callback',
passport.authenticate('facebook', {
failureRedirect: '/login',
successRedirect: '/dashboard'
})
);
app.get('/auth/github',
passport.authenticate('github', { scope: ['user:email'] })
);
app.get('/auth/github/callback',
passport.authenticate('github', {
failureRedirect: '/login',
successRedirect: '/dashboard'
})
);
// 5. Update your database schema to handle multiple provider IDs
// Example Mongoose schema:
const userSchema = new mongoose.Schema({
email: { type: String, unique: true },
name: String,
picture: String,
googleId: String,
facebookId: String,
githubId: String,
// Add more provider IDs as needed
});
// 6. Update findOrCreateUser to handle multiple providers
async function findOrCreateUser(profile) {
// First try to find by provider ID
let user = null;
if (profile.googleId) {
user = await User.findOne({ googleId: profile.googleId });
} else if (profile.facebookId) {
user = await User.findOne({ facebookId: profile.facebookId });
} else if (profile.githubId) {
user = await User.findOne({ githubId: profile.githubId });
}
// If not found by provider ID but email exists, try to find by email
if (!user && profile.email) {
user = await User.findOne({ email: profile.email });
// If user exists with this email, link the new provider ID
if (user) {
if (profile.googleId) user.googleId = profile.googleId;
if (profile.facebookId) user.facebookId = profile.facebookId;
if (profile.githubId) user.githubId = profile.githubId;
await user.save();
}
}
// If user still not found, create a new one
if (!user) {
user = await User.create(profile);
}
return user;
}
Challenges with Social Login
- Account Linking: Users might sign up with different providers using the same email
- Data Consistency: Different providers return different profile information
- Provider Changes: Social providers might change their APIs or policies
- Dependency Risk: Your auth system depends on third-party services
- Account Recovery: Need alternative methods if the social account is lost
Best practice: Always offer a traditional email/password option alongside social login.
OAuth 2.0 in Single Page Applications (SPAs)
Implementing OAuth 2.0 in SPAs presents unique challenges since the client cannot securely store secrets.
SPA Authentication Approaches
| Approach | Description | Pros | Cons |
|---|---|---|---|
| Backend For Frontend (BFF) | Backend proxy that handles OAuth flow and sets session cookies | High security, no client storage of tokens | Requires additional server component |
| Auth Code + PKCE | SPA uses Authorization Code flow with PKCE | Standard approach, no backend required | Requires secure token storage in browser |
| Implicit Flow | Deprecated approach that returns token in URL fragment | Simple implementation | Security vulnerabilities, not recommended |
Recommended: Auth Code + PKCE for SPAs
// Example OAuth implementation in a React application
// 1. Install required packages
// npm install react-router-dom axios
import React, { useState, useEffect, createContext, useContext } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import axios from 'axios';
// Create an auth context
const AuthContext = createContext(null);
// Helper functions for PKCE
function generateCodeVerifier() {
const array = new Uint8Array(32);
window.crypto.getRandomValues(array);
return base64UrlEncode(array);
}
function base64UrlEncode(buffer) {
return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
async function generateCodeChallenge(codeVerifier) {
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const digest = await window.crypto.subtle.digest('SHA-256', data);
return base64UrlEncode(digest);
}
// Auth Provider component
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const navigate = useNavigate();
const location = useLocation();
// Check if we're returning from an OAuth redirect
useEffect(() => {
async function handleRedirect() {
// Check if this is a redirect from OAuth provider
if (location.pathname === '/callback') {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
// Validate state
const storedState = localStorage.getItem('oauth_state');
if (!state || state !== storedState) {
setError('Invalid state parameter');
setLoading(false);
return;
}
// Get code verifier
const codeVerifier = localStorage.getItem('code_verifier');
if (!codeVerifier) {
setError('No code verifier found');
setLoading(false);
return;
}
try {
// Exchange code for tokens
const response = await axios.post('https://authorization-server.com/oauth/token', {
grant_type: 'authorization_code',
code,
redirect_uri: window.location.origin + '/callback',
client_id: 'YOUR_CLIENT_ID',
code_verifier: codeVerifier
});
// Store tokens securely (localStorage is not ideal but common)
localStorage.setItem('access_token', response.data.access_token);
localStorage.setItem('refresh_token', response.data.refresh_token);
localStorage.setItem('token_expiry', Date.now() + response.data.expires_in * 1000);
// Clean up PKCE and state parameters
localStorage.removeItem('oauth_state');
localStorage.removeItem('code_verifier');
// Fetch user info
await fetchUserInfo();
// Redirect to the page they were trying to access
const intendedPath = localStorage.getItem('auth_redirect') || '/dashboard';
localStorage.removeItem('auth_redirect');
navigate(intendedPath);
} catch (err) {
console.error('Authentication error:', err);
setError('Failed to authenticate');
setLoading(false);
}
} else {
// Not a redirect, check if we're already logged in
const token = localStorage.getItem('access_token');
if (token) {
try {
await fetchUserInfo();
} catch (err) {
// Token might be invalid, try refresh token or clear
try {
await refreshTokens();
await fetchUserInfo();
} catch (refreshErr) {
logout();
}
}
}
setLoading(false);
}
}
handleRedirect();
}, [location.pathname]);
// Fetch user info
async function fetchUserInfo() {
try {
const token = localStorage.getItem('access_token');
const response = await axios.get('https://api.example.com/userinfo', {
headers: {
Authorization: `Bearer ${token}`
}
});
setUser(response.data);
setLoading(false);
return response.data;
} catch (err) {
throw err;
}
}
// Refresh tokens
async function refreshTokens() {
try {
const refreshToken = localStorage.getItem('refresh_token');
if (!refreshToken) throw new Error('No refresh token');
const response = await axios.post('https://authorization-server.com/oauth/token', {
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: 'YOUR_CLIENT_ID'
});
localStorage.setItem('access_token', response.data.access_token);
if (response.data.refresh_token) {
localStorage.setItem('refresh_token', response.data.refresh_token);
}
localStorage.setItem('token_expiry', Date.now() + response.data.expires_in * 1000);
return response.data;
} catch (err) {
logout();
throw err;
}
}
// Start login flow
async function login() {
try {
// Generate and store PKCE values
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
// Generate and store state
const state = base64UrlEncode(window.crypto.getRandomValues(new Uint8Array(16)));
// Store values in localStorage
localStorage.setItem('code_verifier', codeVerifier);
localStorage.setItem('oauth_state', state);
// Store the current path for redirect after login
localStorage.setItem('auth_redirect', location.pathname);
// Redirect to authorization server
const authUrl = new URL('https://authorization-server.com/oauth/authorize');
authUrl.searchParams.append('response_type', 'code');
authUrl.searchParams.append('client_id', 'YOUR_CLIENT_ID');
authUrl.searchParams.append('redirect_uri', window.location.origin + '/callback');
authUrl.searchParams.append('scope', 'profile email');
authUrl.searchParams.append('state', state);
authUrl.searchParams.append('code_challenge', codeChallenge);
authUrl.searchParams.append('code_challenge_method', 'S256');
window.location.href = authUrl.toString();
} catch (err) {
console.error('Login error:', err);
setError('Failed to start login flow');
}
}
// Logout
function logout() {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
localStorage.removeItem('token_expiry');
setUser(null);
navigate('/');
}
// Create authenticated HTTP client
function createAuthenticatedClient() {
const client = axios.create();
// Add interceptor to handle token expiration
client.interceptors.request.use(async (config) => {
let token = localStorage.getItem('access_token');
const expiry = localStorage.getItem('token_expiry');
// Check if token is expired or will expire soon
if (expiry && Date.now() > parseInt(expiry) - 60000) {
// Token expired or will expire soon, refresh it
try {
const tokens = await refreshTokens();
token = tokens.access_token;
} catch (err) {
// Refresh failed, redirect to login
logout();
throw err;
}
}
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
return client;
}
// Create the auth value
const value = {
user,
loading,
error,
login,
logout,
createAuthenticatedClient
};
return (
{children}
);
}
// Custom hook to use the auth context
export function useAuth() {
return useContext(AuthContext);
}
// Example component usage
function LoginButton() {
const { login } = useAuth();
return (
);
}
function UserProfile() {
const { user, loading, error, createAuthenticatedClient } = useAuth();
const [profile, setProfile] = useState(null);
useEffect(() => {
async function fetchProfile() {
if (!user) return;
const client = createAuthenticatedClient();
const response = await client.get('https://api.example.com/profile');
setProfile(response.data);
}
fetchProfile();
}, [user]);
if (loading) return Loading...;
if (error) return Error: {error};
if (!user) return Please log in;
return (
Profile: {user.name}
{profile && (
{/* Display profile data */}
)}
);
}
SPA Security Considerations
- Token Storage: Browser storage has security limitations
- XSS Risks: JavaScript access to tokens creates vulnerability
- Token Expiration: Use short-lived access tokens and refresh mechanisms
- Alternative: BFF Pattern: For higher security, consider a Backend For Frontend approach
Practice Activities
Activity 1: Implement Google OAuth Login
Create a simple Express.js application with Google OAuth login:
- Register a new application in the Google Developer Console
- Configure OAuth consent screen and create OAuth credentials
- Implement the Authorization Code flow in your Express.js app
- Use Passport.js or implement the flow directly using axios
- Store user information in a database (MongoDB or similar)
- Create protected routes that require authentication
Bonus: Add a "Login with GitHub" option as well.
Activity 2: Create an OAuth 2.0 Client-Credentials Flow
Build a service-to-service authentication system:
- Create a simple authorization server with Express.js
- Implement the client credentials grant type
- Create a resource server that validates tokens
- Implement a client application that uses the client credentials flow
- Add token expiration and refresh mechanisms
- Test with different client credentials
Activity 3: Implement OAuth in a React SPA
Create a single-page application with OAuth authentication:
- Set up a React application with React Router
- Implement the Authorization Code flow with PKCE
- Create an authentication context for state management
- Implement protected routes that require authentication
- Add token refresh handling
- Create a user profile page showing data from the OAuth provider
Activity 4: Build an OAuth Authorization Server
Create your own basic OAuth 2.0 authorization server:
- Implement client registration and management
- Create authorization endpoint for the authorization code flow
- Implement token endpoint for issuing access and refresh tokens
- Add support for different scopes
- Implement token validation
- Create a simple client application that uses your authorization server
Note: This is an advanced activity and should be approached incrementally.
Additional Resources
- OAuth 2.0 Official Website
- The OAuth 2.0 Authorization Framework (RFC 6749)
- Proof Key for Code Exchange (RFC 7636)
- Auth0 Documentation
- Google OAuth 2.0 Documentation
- OAuth 2.0 Simplified by Aaron Parecki
- Passport.js Authentication Middleware
- OAuth 2.0 for Native Apps (RFC 8252)
- OAuth 2.0 Security Best Practices
Summary
- OAuth 2.0 is an authorization protocol that enables secure, limited access to resources without sharing credentials
- Key roles include: Resource Owner, Client, Authorization Server, and Resource Server
- Main grant types:
- Authorization Code Flow: For server-side applications
- Authorization Code with PKCE: For mobile apps and SPAs
- Client Credentials: For service-to-service communication
- Access tokens provide temporary access to resources, while refresh tokens enable obtaining new access tokens
- Security best practices include:
- Always use HTTPS
- Validate state parameter
- Implement PKCE
- Store tokens securely
- Use proper scopes
- Social login is a common implementation of OAuth 2.0 for user authentication
- SPAs require special consideration for secure implementation
- OAuth 2.0 continues to evolve with security improvements and best practices
Recent Developments in OAuth 2.0
OAuth 2.0 continues to evolve with new extensions and improvements. Here are some of the latest developments:
OAuth 2.1
OAuth 2.1 is not a complete rewrite but a consolidation of the best practices and security improvements that have emerged since OAuth 2.0 was published. Key changes include:
- PKCE Required: PKCE is required for all OAuth clients, not just public clients
- Implicit Flow Removed: The Implicit flow is no longer included due to security concerns
- Resource Owner Password Credentials Removed: This grant type is removed due to its inherent security risks
- Redirect URIs Required: All clients must register redirect URIs
- State Parameter Required: The state parameter is now mandatory for CSRF protection
DPoP (Demonstrating Proof of Possession)
DPoP is an extension that adds a mechanism for preventing token theft. It works by:
- Having the client create a public/private key pair
- Signing a proof of possession with each request
- Binding access tokens to specific clients
Resource Indicators
This extension allows clients to specify which resource server they want to access when requesting tokens, enabling more granular access control and improved token handling for multi-resource scenarios.
JWT Secured Authorization Response Mode (JARM)
JARM adds an extra layer of security by returning authorization responses as signed JWTs, protecting against response tampering and information leakage.
Beyond OAuth: Related Protocols
OAuth 2.0 is often used in conjunction with other protocols to provide a complete identity and access management solution:
OpenID Connect (OIDC)
OpenID Connect is an identity layer built on top of OAuth 2.0. While OAuth 2.0 is about authorization (what you can do), OpenID Connect is about authentication (who you are).
| Feature | OAuth 2.0 | OpenID Connect |
|---|---|---|
| Primary Purpose | Authorization | Authentication |
| Core Concept | Access tokens | ID tokens (JWT) |
| User Info | No standardized way | Standardized claims and userinfo endpoint |
| Discovery | Not defined | Well-defined discovery mechanism |
| Client Registration | Implementation-specific | Standardized dynamic registration |
OpenID Connect Flow
The OIDC flow typically works like this:
- Client initiates authentication with an OpenID provider
- User authenticates and consents to share information
- Provider returns an ID token (JWT) containing identity information
- Client validates the ID token
- Optionally, client may use the access token to get more user info
The ID token contains standardized claims about the user, like name, email, and profile picture.
User-Managed Access (UMA)
UMA is an OAuth-based protocol that enables resource owners to control access to their resources across different services, defining detailed authorization policies.
Financial-grade API (FAPI)
FAPI is a security profile of OAuth 2.0 specifically designed for financial services and other high-value services, adding extra security requirements to mitigate risks specific to those domains.
Common OAuth Implementation Scenarios
Let's explore some common real-world OAuth implementation scenarios and best practices:
Mobile Backend for Frontend (Mobile BFF)
For native mobile apps, a common pattern is to create a dedicated backend for the mobile client that handles OAuth flows:
- Mobile app communicates with its dedicated BFF API
- BFF handles token storage, refresh, and API calls to other services
- Mobile app uses AppAuth SDK or similar for the OAuth flow
- App and BFF communicate over a secure channel
Microservices Architecture
In a microservices environment, OAuth can be implemented in several ways:
- API Gateway Pattern: Gateway handles authentication and passes tokens or claims to services
- Token Propagation: User's token is passed through service calls
- Service-to-Service Auth: Services use Client Credentials flow to communicate
- User Context Propagation: User identity is passed with service calls
Multi-Tenant SaaS Applications
SaaS applications that serve multiple customers often use OAuth for customer-specific authentication:
- Tenant-specific OAuth providers: Support multiple identity providers
- Token exchange: Convert third-party tokens to application-specific tokens
- Custom claims: Add tenant-specific information to tokens
- Role mapping: Map provider roles to application roles
Debugging OAuth Flows
Debugging OAuth flows can be challenging due to their distributed nature. Here are some strategies and tools:
Common OAuth Debug Points
- Authorization Request: Check parameters (client_id, redirect_uri, scope, etc.)
- Redirect URI: Ensure it matches exactly what's registered
- Authorization Response: Look for error parameters or missing code
- Token Request: Verify all parameters and credentials
- Token Response: Check for error responses or invalid tokens
- Resource Access: Check token validity, scopes, and expiration
Debugging Tools
Useful OAuth Debugging Tools
- Browser Dev Tools: Network tab for HTTP requests/responses
- OAuth Debugger: Tools like oauthdebugger.com
- JWT Debugger: Decode and verify tokens at jwt.io
- Postman: Test OAuth flows and API requests
- Charles Proxy/Fiddler: Intercept and analyze HTTP traffic
- OAuth Provider Logs: Check logs in your authorization server
Common OAuth Errors and Solutions
| Error | Possible Causes | Solutions |
|---|---|---|
| invalid_request | Missing or invalid parameter | Check all required parameters and formats |
| invalid_client | Client authentication failed | Verify client_id and client_secret |
| invalid_grant | Authorization code invalid or expired | Check code expiration, ensure one-time use |
| unauthorized_client | Client not authorized for this grant type | Check client configuration in OAuth provider |
| invalid_scope | Requested scope is invalid or unknown | Verify requested scopes are available to your client |
| redirect_uri_mismatch | Redirect URI doesn't match registered URI | Ensure exact match including protocol, path, and case |
| access_denied | User denied the request | Improve consent screen, request fewer permissions |
OAuth in Production
Moving an OAuth implementation to production requires additional considerations:
Production Checklist
- Security Audit: Review implementation against OWASP guidelines and OAuth best practices
- Environment Separation: Use different OAuth clients for development, testing, and production
- Secrets Management: Use a secure vault or environment variables for client secrets
- Monitoring: Implement logging and alerting for OAuth-related events
- Rate Limiting: Protect token endpoints from brute force attacks
- Token Revocation: Implement mechanisms to revoke tokens when needed
- Privacy Considerations: Ensure compliance with privacy regulations
- Documentation: Document your OAuth flows and security practices
Monitoring and Alerting
Key metrics to monitor in your OAuth implementation:
- Token issuance rate: Unusual spikes may indicate abuse
- Token validation failures: High rates may indicate attack attempts
- Token revocations: Track and alert on unusual patterns
- User consent rejections: May indicate UX issues or suspicious requests
- Response times: Slow token issuance can impact user experience
- Error rates: Categorize and track OAuth-related errors
Setting Up OAuth in AWS Environment
Example architecture for a secure OAuth implementation in AWS:
- Use AWS Cognito or Auth0 as your OAuth provider
- Store client secrets in AWS Secrets Manager
- Place OAuth endpoints behind AWS WAF for protection
- Implement AWS CloudWatch alerts for security events
- Use AWS Lambda for token validation and processing
- Consider AWS API Gateway for API protection
Conclusion
OAuth 2.0 has become the industry standard for authorization, enabling secure access to resources across different services. By understanding the core flows and security considerations, you can implement robust authentication and authorization in your applications.
Key takeaways:
- Choose the appropriate OAuth flow for your application type
- Implement security best practices, especially PKCE for public clients
- Properly manage tokens with secure storage and refresh mechanisms
- Stay updated with the latest OAuth security recommendations
- Consider OpenID Connect for authentication needs
As we move forward in our authentication journey, we'll explore how to integrate OAuth with specific providers and implement more advanced authentication patterns.
Homework Assignment
To solidify your understanding of OAuth 2.0, complete the following assignment:
OAuth Provider Comparison
- Choose three popular OAuth providers (e.g., Google, GitHub, Auth0, Okta)
- Create developer accounts and register a test application with each
- Document the setup process, required settings, and available options
- Compare the scopes/permissions available from each provider
- Implement a basic authentication flow with each provider
- Test the implementations and document any differences
- Create a comparison table highlighting the pros and cons of each provider
- Present your findings in a short report or presentation
This assignment will give you hands-on experience with real OAuth providers and help you understand the practical considerations when implementing OAuth in production applications.
Next Class: Passport.js Strategies
In our next session, we'll explore Passport.js, a popular authentication middleware for Node.js that makes implementing various OAuth strategies simpler. We'll cover:
- Passport.js architecture and concepts
- Implementing OAuth strategies
- Local authentication integration
- Session management with Passport
- Custom authentication strategies
- Best practices for Passport.js applications
Come prepared with your OAuth knowledge, as we'll build on the concepts we've covered today.