Introduction to OAuth 2.0
OAuth 2.0 is an authorization framework that enables third-party applications to obtain limited access to a user's account on a server. It's designed to work with HTTP and provides specific authorization flows for web applications, desktop applications, mobile phones, and IoT devices.
Think of OAuth like a valet key for your car. With a regular key, the valet can drive your car anywhere. But with a valet key, they can only drive your car but not access the trunk or glove compartment. Similarly, OAuth allows you to give third-party applications a "valet key" to your data without handing over your "master key" (your username and password).
Key Players in OAuth 2.0
- Resource Owner: The user who owns the data or resource (like a Google Drive account)
- Client: The application requesting access to the resource (like a calendar app)
- Authorization Server: The server that authenticates the Resource Owner and issues access tokens (like Google's authentication servers)
- Resource Server: The server hosting the protected resources (like Google Drive's API)
OAuth 2.0 Authorization Flows
OAuth 2.0 defines several "grant types" or flows for different use cases. Let's explore the most common ones:
Authorization Code Flow
The most secure and common flow, ideal for server-side web applications.
This flow is like applying for a temporary building pass at a secure facility:
- You (the User) tell the receptionist (Client) you need access
- The receptionist sends you to security (Auth Server)
- Security verifies your ID and checks if you're allowed in
- Security gives you a temporary pass request slip (Auth Code)
- You bring this slip back to the receptionist
- The receptionist calls security to confirm the slip is legitimate
- Security issues a temporary building pass (Access Token)
- The receptionist uses this pass to escort you into the building (Resource Server)
// NodeJS implementation with Express and Passport
const express = require('express');
const passport = require('passport');
const OAuth2Strategy = require('passport-oauth2').Strategy;
const session = require('express-session');
const app = express();
// Session setup
app.use(session({
secret: 'your-session-secret',
resave: false,
saveUninitialized: false
}));
// Initialize Passport
app.use(passport.initialize());
app.use(passport.session());
passport.serializeUser((user, done) => {
done(null, user);
});
passport.deserializeUser((obj, done) => {
done(null, obj);
});
// Configure OAuth 2.0 strategy
passport.use(new OAuth2Strategy({
authorizationURL: 'https://authorization-server.com/auth',
tokenURL: 'https://authorization-server.com/token',
clientID: 'YOUR_CLIENT_ID',
clientSecret: 'YOUR_CLIENT_SECRET',
callbackURL: 'http://localhost:3000/auth/callback'
},
function(accessToken, refreshToken, profile, done) {
// Store tokens and profile information
return done(null, {
accessToken,
refreshToken,
profile
});
}
));
// Routes
app.get('/', (req, res) => {
if (req.isAuthenticated()) {
res.send('Logged in! <a href="/logout">Logout</a>');
} else {
res.send('Not logged in. <a href="/auth">Login</a>');
}
});
// Start authorization flow
app.get('/auth', passport.authenticate('oauth2'));
// Authorization callback
app.get('/auth/callback',
passport.authenticate('oauth2', { failureRedirect: '/' }),
(req, res) => {
// Successful authentication
res.redirect('/');
}
);
// Access protected resource
app.get('/resource', (req, res) => {
if (!req.isAuthenticated()) {
return res.redirect('/');
}
// Use the access token to request data from the resource server
fetch('https://resource-server.com/api/data', {
headers: {
'Authorization': `Bearer ${req.user.accessToken}`
}
})
.then(response => response.json())
.then(data => {
res.json(data);
})
.catch(error => {
res.status(500).json({ error: 'Failed to fetch resource' });
});
});
app.get('/logout', (req, res) => {
req.logout();
res.redirect('/');
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Implicit Flow (Not Recommended for New Applications)
A simplified flow designed for public clients that can't securely store client secrets (like JavaScript applications). This flow is no longer recommended due to security concerns.
This flow is like getting a visitor badge at an office that's printed immediately upon showing your ID - there's no intermediate verification step.
Client Credentials Flow
Used for server-to-server authentication where no user is involved.
This flow is similar to two systems exchanging API keys or service credentials - like one corporate system authenticating to another using its own identity rather than a user's.
// Client Credentials Flow with Node.js
const axios = require('axios');
const qs = require('querystring');
async function getSystemAccessToken() {
try {
const response = await axios.post('https://authorization-server.com/token',
qs.stringify({
grant_type: 'client_credentials',
client_id: 'SYSTEM_CLIENT_ID',
client_secret: 'SYSTEM_CLIENT_SECRET',
scope: 'read write'
}),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
return response.data.access_token;
} catch (error) {
console.error('Error getting system access token:', error);
throw error;
}
}
async function fetchResourceAsSystem() {
try {
const accessToken = await getSystemAccessToken();
const resourceResponse = await axios.get('https://resource-server.com/api/data', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
return resourceResponse.data;
} catch (error) {
console.error('Error fetching resource:', error);
throw error;
}
}
// Usage in an API service
app.get('/system-data', async (req, res) => {
try {
const data = await fetchResourceAsSystem();
res.json(data);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch system data' });
}
});
Resource Owner Password Credentials Flow
Used when there is a high degree of trust between the user and the client. The user provides their credentials directly to the client.
This is like lending your actual house key to a trusted friend - they get full access, but only because you trust them completely.
This flow is only appropriate for first-party applications where the client is developed by the same organization that controls the authorization server.
Authorization Code Flow with PKCE
A secure extension to the Authorization Code flow for public clients (like mobile and single-page applications).
PKCE (Proof Key for Code Exchange, pronounced "pixy") adds protection against code interception attacks by having the client prove it's the same application that initiated the flow.
It's like adding a unique password to your temporary pass request slip (the authorization code) so that only you can redeem it, even if someone else steals the slip.
// PKCE flow in a JavaScript SPA
// This would be part of your authentication service
class AuthService {
constructor() {
this.auth0Domain = 'your-domain.auth0.com';
this.clientId = 'YOUR_CLIENT_ID';
this.redirectUri = 'http://localhost:3000/callback';
// Store in memory during the authorization flow
this.codeVerifier = null;
this.accessToken = null;
this.refreshToken = null;
}
// Generate a random string for the code verifier
generateCodeVerifier() {
const array = new Uint8Array(32);
window.crypto.getRandomValues(array);
return this.base64URLEncode(array);
}
// Create a code challenge from the verifier
async generateCodeChallenge(codeVerifier) {
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const digest = await window.crypto.subtle.digest('SHA-256', data);
return this.base64URLEncode(new Uint8Array(digest));
}
base64URLEncode(array) {
return btoa(String.fromCharCode.apply(null, array))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
// Start the authentication flow
async login() {
// Generate and store PKCE code verifier
this.codeVerifier = this.generateCodeVerifier();
// Generate code challenge
const codeChallenge = await this.generateCodeChallenge(this.codeVerifier);
// Store code verifier in local storage or session storage
localStorage.setItem('code_verifier', this.codeVerifier);
// Build authorization URL
const authUrl = new URL(`https://${this.auth0Domain}/authorize`);
authUrl.searchParams.append('client_id', this.clientId);
authUrl.searchParams.append('response_type', 'code');
authUrl.searchParams.append('redirect_uri', this.redirectUri);
authUrl.searchParams.append('scope', 'openid profile email offline_access');
authUrl.searchParams.append('code_challenge', codeChallenge);
authUrl.searchParams.append('code_challenge_method', 'S256');
// Redirect to authorization server
window.location.href = authUrl.toString();
}
// Handle the callback from authorization server
async handleCallback() {
// Get authorization code from URL
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
if (code) {
// Retrieve code verifier from storage
const codeVerifier = localStorage.getItem('code_verifier');
// Exchange code for tokens
try {
const tokenResponse = await fetch(`https://${this.auth0Domain}/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: this.clientId,
code_verifier: codeVerifier,
code: code,
redirect_uri: this.redirectUri
})
});
if (!tokenResponse.ok) {
throw new Error('Failed to exchange code for tokens');
}
const tokens = await tokenResponse.json();
// Store tokens securely (consider memory-only storage for production)
this.accessToken = tokens.access_token;
this.refreshToken = tokens.refresh_token;
// Clear code verifier from storage
localStorage.removeItem('code_verifier');
// Redirect to application
window.location.href = '/';
} catch (error) {
console.error('Token exchange error:', error);
}
}
}
// Get user resource with access token
async getResource() {
if (!this.accessToken) {
throw new Error('No access token available');
}
try {
const response = await fetch('https://resource-server.com/api/data', {
headers: {
'Authorization': `Bearer ${this.accessToken}`
}
});
if (!response.ok) {
throw new Error('Failed to fetch resource');
}
return await response.json();
} catch (error) {
console.error('Resource fetch error:', error);
throw error;
}
}
}
OAuth 2.0 Tokens
OAuth 2.0 uses different types of tokens for authorization and access control.
Access Tokens
Access tokens allow a client to access protected resources. They can be in any format, but are commonly JWTs in modern implementations.
- Purpose: Used to access protected resources on the resource server
- Lifetime: Short (minutes to hours) to limit damage if compromised
- Storage: Should be kept secure and not accessible to third parties
- Usage: Typically sent in the Authorization header as a Bearer token
// Example of using an access token
fetch('https://api.example.com/user/profile', {
headers: {
'Authorization': 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...'
}
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
Refresh Tokens
Refresh tokens are used to obtain new access tokens when the current one expires.
- Purpose: To obtain new access tokens without requiring the user to re-authenticate
- Lifetime: Longer than access tokens (days to months)
- Storage: Must be stored securely, preferably in HTTP-only cookies or secure storage
- Usage: Sent to the authorization server when requesting a new access token
// Example of using a refresh token to get a new access token
async function refreshAccessToken(refreshToken) {
try {
const response = await fetch('https://authorization-server.com/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: 'YOUR_CLIENT_ID',
client_secret: 'YOUR_CLIENT_SECRET'
})
});
if (!response.ok) {
throw new Error('Token refresh failed');
}
const data = await response.json();
return {
accessToken: data.access_token,
refreshToken: data.refresh_token || refreshToken // Some servers issue new refresh tokens
};
} catch (error) {
console.error('Error refreshing token:', error);
throw error;
}
}
ID Tokens
Specific to OpenID Connect (an extension of OAuth 2.0), ID tokens contain user identity information.
- Purpose: Provide information about the user's identity
- Format: Always a JWT (JSON Web Token)
- Contents: Claims about the user (name, email, etc.) and authentication metadata
- Usage: Client-side only, not meant to be sent to resource servers
// Example ID Token payload
{
"iss": "https://authorization-server.com", // Issuer
"sub": "user123", // Subject (user identifier)
"aud": "client456", // Audience (client ID)
"exp": 1619155200, // Expiration time
"iat": 1619151600, // Issued at time
"auth_time": 1619151590, // Time of authentication
"nonce": "n-0S6_WzA2Mj", // Prevents replay attacks
"name": "John Doe", // User's full name
"given_name": "John", // First name
"family_name": "Doe", // Last name
"email": "john.doe@example.com", // Email
"email_verified": true // Email verification status
}
OAuth 2.0 Scopes
Scopes are a way to limit an application's access to a user's account. They specify what actions an application can perform on behalf of the user.
Think of scopes like permissions on your smartphone. When you install an app, it might request access to your camera, contacts, location, etc. You can grant or deny each permission individually, and the app can only access what you've explicitly allowed.
Common OAuth 2.0 Scopes
profile: Access to basic profile informationemail: Access to the user's email addressopenid: Indicates that the client wants to use OpenID Connect for authenticationoffline_access: Request a refresh token for long-term accessread/write: Basic read or write permissions
Each service provider defines its own set of scopes. For example, Google has scopes like https://www.googleapis.com/auth/drive.readonly for read-only access to Google Drive files.
// Example of requesting specific scopes during authorization
function redirectToAuth() {
const authUrl = new URL('https://authorization-server.com/auth');
authUrl.searchParams.append('client_id', 'YOUR_CLIENT_ID');
authUrl.searchParams.append('redirect_uri', 'https://your-app.com/callback');
authUrl.searchParams.append('response_type', 'code');
authUrl.searchParams.append('scope', 'openid profile email calendar.read contacts.write');
window.location.href = authUrl.toString();
}
Implementing Scope Checking
On the resource server, you need to verify that the access token has the necessary scopes for the requested operation.
// Express middleware to check scopes
function requireScopes(requiredScopes) {
return (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Access token required' });
}
const token = authHeader.split(' ')[1];
try {
// Decode and verify JWT (implementation details vary)
const decodedToken = jwt.verify(token, 'YOUR_SECRET_KEY');
// Check if token has required scopes
const tokenScopes = decodedToken.scope.split(' ');
const hasRequiredScopes = requiredScopes.every(scope =>
tokenScopes.includes(scope)
);
if (!hasRequiredScopes) {
return res.status(403).json({
error: 'Insufficient permissions',
required_scopes: requiredScopes,
provided_scopes: tokenScopes
});
}
// Add token payload to request for later use
req.user = decodedToken;
next();
} catch (error) {
return res.status(401).json({ error: 'Invalid token' });
}
};
}
// Usage in routes
app.get('/api/profile',
requireScopes(['profile']),
(req, res) => {
// Only accessible with 'profile' scope
res.json({ name: req.user.name, email: req.user.email });
}
);
app.post('/api/calendar/events',
requireScopes(['calendar.write']),
(req, res) => {
// Only accessible with 'calendar.write' scope
// Create calendar event...
res.json({ status: 'Event created' });
}
);
Introduction to OpenID Connect
OpenID Connect (OIDC) is an identity layer built on top of OAuth 2.0, adding authentication capabilities to the authorization framework.
While OAuth 2.0 is designed for authorization ("what can you do?"), OpenID Connect is designed for authentication ("who are you?"). It enables a client to verify a user's identity and get basic profile information through a standardized REST-like API.
Continuing our analogy, if OAuth is like a valet key that grants limited access to your car, OpenID Connect is like adding a driver's license check before issuing that key - it verifies who the person is before granting them access.
Key Components of OpenID Connect
- ID Token: A JWT containing user identity information
- UserInfo Endpoint: An API that returns claims about the authenticated user
- Standard Claims: Predefined user attributes like sub (subject), name, email, etc.
- Discovery: A mechanism for clients to dynamically discover OpenID Provider configuration
- Dynamic Registration: Allows clients to register with an OpenID Provider programmatically
OpenID Connect Flows
OpenID Connect defines three authentication flows, built on top of the corresponding OAuth 2.0 flows:
Authorization Code Flow
The most secure flow, suitable for server-side applications.
Implicit Flow
Less secure, designed for browser-based applications (not recommended for new implementations).
Hybrid Flow
Combines aspects of both flows, returning some tokens from the authorization endpoint and others from the token endpoint.
Implementing OpenID Connect with Auth0
Let's look at a practical implementation of OpenID Connect using Auth0, a popular identity platform.
Setting Up Auth0
- Create an Auth0 account at auth0.com
- Create a new application (Regular Web Application for server-side apps or Single Page Application for client-side apps)
- Configure Allowed Callback URLs, Logout URLs, and Web Origins
- Note your Domain, Client ID, and Client Secret
Server-Side Implementation (Node.js/Express)
// npm install express express-session passport passport-auth0
const express = require('express');
const session = require('express-session');
const passport = require('passport');
const Auth0Strategy = require('passport-auth0');
const app = express();
// Configure session
app.use(session({
secret: 'your-session-secret',
resave: false,
saveUninitialized: false
}));
// Initialize Passport
app.use(passport.initialize());
app.use(passport.session());
// Configure Auth0 strategy
passport.use(new Auth0Strategy({
domain: 'your-auth0-domain.auth0.com',
clientID: 'YOUR_CLIENT_ID',
clientSecret: 'YOUR_CLIENT_SECRET',
callbackURL: 'http://localhost:3000/callback',
scope: 'openid profile email'
},
function(accessToken, refreshToken, extraParams, profile, done) {
// extraParams.id_token has the ID token
return done(null, {
...profile,
id_token: extraParams.id_token,
access_token: accessToken,
refresh_token: refreshToken
});
}
));
// Serialize/deserialize user
passport.serializeUser((user, done) => {
done(null, user);
});
passport.deserializeUser((user, done) => {
done(null, user);
});
// Routes
app.get('/', (req, res) => {
res.send(req.isAuthenticated() ?
`Logged in as ${req.user.displayName} <a href="/logout">Logout</a>` :
'Not logged in <a href="/login">Login</a>');
});
// Start login
app.get('/login', passport.authenticate('auth0', {
scope: 'openid profile email'
}));
// Handle callback
app.get('/callback',
passport.authenticate('auth0', { failureRedirect: '/' }),
(req, res) => {
res.redirect('/profile');
}
);
// Protected profile route
app.get('/profile', ensureAuthenticated, (req, res) => {
res.json({
user: req.user._json,
id_token: req.user.id_token
});
});
// Logout
app.get('/logout', (req, res) => {
req.logout();
// Redirect to Auth0 logout to end the session
const returnTo = encodeURIComponent('http://localhost:3000');
res.redirect(`https://your-auth0-domain.auth0.com/v2/logout?client_id=${process.env.AUTH0_CLIENT_ID}&returnTo=${returnTo}`);
});
// Middleware to check if user is authenticated
function ensureAuthenticated(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
res.redirect('/login');
}
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Client-Side Implementation (React)
// npm install @auth0/auth0-react
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { Auth0Provider } from '@auth0/auth0-react';
ReactDOM.render(
,
document.getElementById('root')
);
// src/App.js
import React from 'react';
import { useAuth0 } from '@auth0/auth0-react';
import Profile from './Profile';
import LoginButton from './LoginButton';
import LogoutButton from './LogoutButton';
function App() {
const { isAuthenticated, isLoading } = useAuth0();
if (isLoading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Auth0 OIDC Demo</h1>
{isAuthenticated ? (
<>
<LogoutButton />
<Profile />
</>
) : (
<LoginButton />
)}
</div>
);
}
export default App;
// src/LoginButton.js
import React from 'react';
import { useAuth0 } from '@auth0/auth0-react';
const LoginButton = () => {
const { loginWithRedirect } = useAuth0();
return <button onClick={() => loginWithRedirect()}>Log In</button>;
};
export default LoginButton;
// src/LogoutButton.js
import React from 'react';
import { useAuth0 } from '@auth0/auth0-react';
const LogoutButton = () => {
const { logout } = useAuth0();
return (
<button onClick={() => logout({ returnTo: window.location.origin })}>
Log Out
</button>
);
};
export default LogoutButton;
// src/Profile.js
import React, { useEffect, useState } from 'react';
import { useAuth0 } from '@auth0/auth0-react';
const Profile = () => {
const { user, isAuthenticated, getIdTokenClaims, getAccessTokenSilently } = useAuth0();
const [idToken, setIdToken] = useState(null);
const [apiData, setApiData] = useState(null);
useEffect(() => {
async function getTokens() {
if (isAuthenticated) {
// Get ID token claims
const claims = await getIdTokenClaims();
setIdToken(claims);
// Call protected API with access token
try {
const accessToken = await getAccessTokenSilently();
const response = await fetch('https://your-api.com/data', {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
const data = await response.json();
setApiData(data);
} catch (error) {
console.error('API call failed:', error);
}
}
}
getTokens();
}, [isAuthenticated, getIdTokenClaims, getAccessTokenSilently]);
if (!isAuthenticated) {
return <div>Please log in</div>;
}
return (
Profile
<img src={user.picture} alt={user.name} />
{user.name}
{user.email}
ID Token Claims
{JSON.stringify(idToken, null, 2)}
API Data
{JSON.stringify(apiData, null, 2)}
);
};
export default Profile;
ID Token Validation
Properly validating the ID token is crucial for security in OpenID Connect implementations.
Required Validation Steps
- Verify signature: Check that the token was signed by the expected issuer
- Validate issuer: Ensure the
issclaim matches your expected issuer - Validate audience: Check that the
audclaim includes your client ID - Check expiration: Verify the
expclaim to ensure the token hasn't expired - Check issue time: Verify the
iatclaim to ensure the token wasn't issued in the future - Verify nonce: If a nonce was sent with the auth request, validate it against the
nonceclaim
// ID Token validation in Node.js
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
// Setup the JWKS client
const client = jwksClient({
jwksUri: `https://your-auth0-domain.auth0.com/.well-known/jwks.json`
});
// Function to get the signing key
function getSigningKey(kid) {
return new Promise((resolve, reject) => {
client.getSigningKey(kid, (err, key) => {
if (err) {
return reject(err);
}
const signingKey = key.getPublicKey();
resolve(signingKey);
});
});
}
// Validate ID token
async function validateIdToken(idToken) {
try {
// Decode the token without verification to get the header
const decoded = jwt.decode(idToken, { complete: true });
if (!decoded || !decoded.header || !decoded.header.kid) {
throw new Error('Invalid token');
}
// Get the signing key
const signingKey = await getSigningKey(decoded.header.kid);
// Verify the token
const verified = jwt.verify(idToken, signingKey, {
algorithms: ['RS256'],
audience: 'YOUR_CLIENT_ID',
issuer: 'https://your-auth0-domain.auth0.com/'
});
// Additional checks
const now = Math.floor(Date.now() / 1000);
if (verified.exp < now) {
throw new Error('Token expired');
}
if (verified.iat > now) {
throw new Error('Token issued in the future');
}
// If nonce was used, verify it
if (sessionStorage.getItem('nonce') && verified.nonce !== sessionStorage.getItem('nonce')) {
throw new Error('Invalid nonce');
}
return verified;
} catch (error) {
console.error('Token validation error:', error);
throw error;
}
}
Security Considerations
Both OAuth 2.0 and OpenID Connect have potential security vulnerabilities that need to be addressed.
Common Security Threats
- Cross-Site Request Forgery (CSRF): Using state parameter to prevent forgery
- Authorization Code Interception: Using PKCE to prevent code interception
- Token Leakage: Proper token storage and transport
- Redirect URI Manipulation: Strict validation of redirect URIs
- Access Token Theft: Using short-lived tokens and proper storage
- Phishing Attacks: Educating users about authentication flows
State Parameter
The state parameter is used to prevent CSRF attacks. It's a random value sent in the authorization request and returned unchanged in the response.
// Generate and use state parameter
function startAuthentication() {
// Generate random state
const state = generateRandomString(32);
// Store state in browser for later verification
sessionStorage.setItem('oauth_state', state);
// Include state in authorization request
const authUrl = new URL('https://authorization-server.com/auth');
authUrl.searchParams.append('client_id', 'YOUR_CLIENT_ID');
authUrl.searchParams.append('redirect_uri', 'https://your-app.com/callback');
authUrl.searchParams.append('response_type', 'code');
authUrl.searchParams.append('scope', 'openid profile email');
authUrl.searchParams.append('state', state);
// Redirect to authorization server
window.location.href = authUrl.toString();
}
// Verify state in callback
function handleCallback() {
const urlParams = new URLSearchParams(window.location.search);
const state = urlParams.get('state');
const code = urlParams.get('code');
// Verify state matches
const savedState = sessionStorage.getItem('oauth_state');
sessionStorage.removeItem('oauth_state'); // Clear stored state
if (!state || state !== savedState) {
// Potential CSRF attack
console.error('State validation failed');
return;
}
// Proceed with code exchange
exchangeCodeForTokens(code);
}
function generateRandomString(length) {
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
const randomValues = new Uint8Array(length);
window.crypto.getRandomValues(randomValues);
randomValues.forEach(val => {
result += charset[val % charset.length];
});
return result;
}
Best Practices
- Always use HTTPS: Secure communication is essential
- Validate all tokens: Never trust tokens without verification
- Use PKCE: Even for confidential clients, PKCE adds an extra layer of security
- Short token lifetimes: Limit the window of opportunity for token misuse
- Secure token storage: Use appropriate storage mechanisms (HTTP-only cookies, secure storage)
- Minimal scopes: Request only the scopes you need
- Validate redirect URIs: Strict matching of registered redirect URIs
Building Your Own OAuth 2.0 / OIDC Provider
While most applications use existing OAuth providers, understanding how to build your own can provide valuable insights.
Key Components of an OAuth Provider
- User Database: Store user accounts and credentials
- Client Registry: Store registered client applications
- Authorization Endpoint: Handle authorization requests and user consent
- Token Endpoint: Issue tokens in exchange for authorization codes or refresh tokens
- Token Storage: Manage issued tokens (if using reference tokens)
- UserInfo Endpoint: Provide user profile information (for OIDC)
- Discovery Document: Publish provider metadata (for OIDC)
Building an OAuth Provider with Node.js
Let's explore a simplified implementation using the oauth2-server library:
// npm install express oauth2-server
const express = require('express');
const OAuth2Server = require('oauth2-server');
const Request = OAuth2Server.Request;
const Response = OAuth2Server.Response;
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// In-memory storage (use database in production)
const users = [
{ id: '1', username: 'user1', password: 'password1' }
];
const clients = [
{ id: 'client1', secret: 'secret1', redirectUris: ['http://localhost:3000/callback'] }
];
const authorizationCodes = [];
const tokens = [];
// OAuth server model
const model = {
// Get client by ID
getClient: function(clientId, clientSecret) {
const client = clients.find(c => c.id === clientId);
if (!client || (clientSecret && client.secret !== clientSecret)) {
return false;
}
return {
id: client.id,
redirectUris: client.redirectUris,
grants: ['authorization_code', 'refresh_token']
};
},
// Save authorization code
saveAuthorizationCode: function(code, client, user) {
const authCode = {
authorizationCode: code.authorizationCode,
expiresAt: code.expiresAt,
redirectUri: code.redirectUri,
scope: code.scope,
client: { id: client.id },
user: { id: user.id }
};
authorizationCodes.push(authCode);
return authCode;
},
// Get authorization code
getAuthorizationCode: function(authorizationCode) {
return authorizationCodes.find(code => code.authorizationCode === authorizationCode);
},
// Revoke authorization code
revokeAuthorizationCode: function(code) {
const index = authorizationCodes.findIndex(c =>
c.authorizationCode === code.authorizationCode
);
if (index !== -1) {
authorizationCodes.splice(index, 1);
return true;
}
return false;
},
// Save token
saveToken: function(token, client, user) {
const newToken = {
accessToken: token.accessToken,
accessTokenExpiresAt: token.accessTokenExpiresAt,
refreshToken: token.refreshToken,
refreshTokenExpiresAt: token.refreshTokenExpiresAt,
scope: token.scope,
client: { id: client.id },
user: { id: user.id }
};
tokens.push(newToken);
return newToken;
},
// Get access token
getAccessToken: function(accessToken) {
return tokens.find(token => token.accessToken === accessToken);
},
// Get refresh token
getRefreshToken: function(refreshToken) {
return tokens.find(token => token.refreshToken === refreshToken);
},
// Revoke refresh token
revokeToken: function(token) {
const index = tokens.findIndex(t => t.refreshToken === token.refreshToken);
if (index !== -1) {
tokens.splice(index, 1);
return true;
}
return false;
},
// Verify scope
verifyScope: function(token, scope) {
if (!token.scope) {
return false;
}
const requestedScopes = scope.split(' ');
const authorizedScopes = token.scope.split(' ');
return requestedScopes.every(s => authorizedScopes.includes(s));
},
// Get user
getUser: function(username, password) {
const user = users.find(u => u.username === username);
if (!user || user.password !== password) {
return false;
}
return { id: user.id };
},
// Get user from client
getUserFromClient: function(client) {
// For client credentials grant (not implemented in this example)
return false;
}
};
// Create OAuth server
const oauth = new OAuth2Server({
model: model,
accessTokenLifetime: 3600, // 1 hour
refreshTokenLifetime: 1209600, // 14 days
allowBearerTokensInQueryString: true
});
// Middleware to authenticate requests
function authenticateHandler(req, res, next) {
const request = new Request(req);
const response = new Response(res);
return oauth.authenticate(request, response)
.then(token => {
req.user = token.user;
req.client = token.client;
req.scope = token.scope;
next();
})
.catch(err => {
res.status(err.code || 500).json(err);
});
}
// Authorization endpoint
app.get('/authorize', async (req, res) => {
const { client_id, redirect_uri, response_type, scope, state } = req.query;
// In a real implementation, you would:
// 1. Authenticate the user (login form)
// 2. Show consent screen
// 3. Process user's decision
// For simplicity, we'll skip the UI and assume user consent
// Validate client and redirect URI
const client = clients.find(c => c.id === client_id);
if (!client || !client.redirectUris.includes(redirect_uri)) {
return res.status(400).json({ error: 'invalid_client' });
}
if (response_type !== 'code') {
return res.status(400).json({ error: 'unsupported_response_type' });
}
// Generate authorization code
const authCode = {
authorizationCode: Math.random().toString(36).substring(2, 15),
expiresAt: new Date(Date.now() + 600000), // 10 minutes
redirectUri: redirect_uri,
scope: scope || '',
client: { id: client_id },
user: { id: '1' } // Assume user 1 for this example
};
authorizationCodes.push(authCode);
// Redirect with code
const redirectUrl = new URL(redirect_uri);
redirectUrl.searchParams.append('code', authCode.authorizationCode);
if (state) {
redirectUrl.searchParams.append('state', state);
}
res.redirect(redirectUrl.toString());
});
// Token endpoint
app.post('/token', (req, res) => {
const request = new Request(req);
const response = new Response(res);
return oauth.token(request, response)
.then(token => {
res.json({
access_token: token.accessToken,
token_type: 'Bearer',
expires_in: Math.floor((token.accessTokenExpiresAt - new Date()) / 1000),
refresh_token: token.refreshToken,
scope: token.scope
});
})
.catch(err => {
res.status(err.code || 500).json(err);
});
});
// Protected resource endpoint
app.get('/resource', authenticateHandler, (req, res) => {
res.json({
message: 'Protected resource',
user_id: req.user.id
});
});
// UserInfo endpoint (for OpenID Connect)
app.get('/userinfo', authenticateHandler, (req, res) => {
const user = users.find(u => u.id === req.user.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
// Only return fields authorized by scope
const scope = req.scope ? req.scope.split(' ') : [];
const userinfo = {
sub: user.id
};
if (scope.includes('profile')) {
userinfo.name = 'User One';
userinfo.preferred_username = user.username;
}
if (scope.includes('email')) {
userinfo.email = 'user1@example.com';
userinfo.email_verified = true;
}
res.json(userinfo);
});
// OpenID Connect discovery document
app.get('/.well-known/openid-configuration', (req, res) => {
const baseUrl = 'http://localhost:3000';
res.json({
issuer: baseUrl,
authorization_endpoint: `${baseUrl}/authorize`,
token_endpoint: `${baseUrl}/token`,
userinfo_endpoint: `${baseUrl}/userinfo`,
jwks_uri: `${baseUrl}/.well-known/jwks.json`,
response_types_supported: ['code'],
subject_types_supported: ['public'],
id_token_signing_alg_values_supported: ['RS256'],
scopes_supported: ['openid', 'profile', 'email'],
token_endpoint_auth_methods_supported: ['client_secret_basic'],
claims_supported: ['sub', 'name', 'preferred_username', 'email', 'email_verified']
});
});
app.listen(3000, () => {
console.log('OAuth 2.0 / OIDC server running on port 3000');
});
This is a simplified example. A production-ready OAuth provider would include:
- User authentication UI
- Consent screen
- Database storage for clients, tokens, etc.
- Proper crypto for tokens (issuing JWTs)
- JWKS endpoint for key distribution
- Client registration endpoints
- More comprehensive error handling
Real-World Use Cases
Single Sign-On (SSO)
Using a single identity provider to authenticate across multiple applications.
Example: A corporation using Azure AD as the identity provider for its internal applications.
Third-Party API Access
Allowing third-party applications to access a platform's API on behalf of users.
Example: Using OAuth to allow a third-party application to post tweets on a user's behalf via the Twitter API.
Microservices Authentication
Using OAuth/OIDC for authentication and authorization in a microservices architecture.
Example: An e-commerce platform where the API gateway validates tokens before forwarding requests to microservices.
Mobile App Authentication
Using OAuth/OIDC to authenticate users in mobile applications.
Example: A mobile banking app that uses OAuth 2.0 with PKCE to authenticate users.
Practical Activities
Activity 1: Implement Social Login
Add Google or Facebook login to an existing application:
- Register your application with the provider (Google or Facebook)
- Implement the OAuth flow using Passport.js or a similar library
- Store user profile information and link it to your application's user accounts
- Add a "Login with Google/Facebook" button to your UI
- Implement logout functionality
Activity 2: Create an OAuth Client
Build a simple client application that uses OAuth for authentication:
- Choose an OAuth provider (Auth0, Okta, or build your own)
- Implement the authorization code flow with PKCE
- Store tokens securely and handle token refresh
- Create a protected resource endpoint that requires a valid token
- Implement proper error handling and security measures
Activity 3: Build a Simple OpenID Connect Provider
Extend the OAuth provider example to support OpenID Connect:
- Add support for the
openidscope - Implement ID token generation with JWT
- Create a UserInfo endpoint
- Add the discovery document endpoint
- Implement a JWKS endpoint for key distribution
Activity 4: Implement Single Sign-On
Create a simple SSO system with multiple applications:
- Set up an identity provider (Auth0, Okta, or your own)
- Create two or more client applications
- Configure all applications to use the same identity provider
- Implement login and session management
- Test single sign-on functionality across applications
Additional Resources
Specifications
- OAuth 2.0 Framework (RFC 6749)
- OAuth 2.0 Bearer Token Usage (RFC 6750)
- PKCE Extension (RFC 7636)
- OpenID Connect Core 1.0
- OpenID Connect Discovery
Libraries and Tools
- Passport.js - Authentication middleware for Node.js
- node-oidc-provider - OpenID Connect Provider implementation
- simple-oauth2 - Node.js OAuth 2.0 client
- auth0-spa-js - Auth0 SDK for Single Page Applications
- AppAuth-JS - OAuth client implementation with PKCE support