OAuth Essentials
Quick reference guide for OAuth 2.0 and OpenID Connect fundamentals.
What is OAuth?
OAuth 2.0 is an authorization framework that:
Key Concepts
Roles
┌──────────┐ ┌──────────────┐
│ Resource │ │ Authorization│
│ Owner │ │ Server │
│ (User) │ │ │
└──────────┘ └──────────────┘
│ │
│ 1. Authorization Request │
│──────────────────────────────────────────────►│
│ │
│ 2. Authorization Grant │
│◄──────────────────────────────────────────────│
│ │
┌──────────┐ ┌──────────────┐
│ Client │ │ Resource │
│Application│ │ Server │
│ │ │ (API) │
└──────────┘ └──────────────┘
│ │
│ 3. Access Token Request │
│──────────────────────────────────────────────►│
│ │
│ 4. Access Token │
│◄──────────────────────────────────────────────│
│ │
│ 5. Access Resource │
│──────────────────────────────────────────────►│
│ │
│ 6. Protected Resource │
│◄──────────────────────────────────────────────│
Tokens
Scopes
Define the level of access:
read:user # Read user profile
write:repo # Write to repositories
admin:org # Admin organization access
openid profile # OpenID Connect scopes
OAuth 2.0 Flows
1. Authorization Code Flow (Most Secure)
Use Case: Server-side web applications
User Client Auth Server Resource Server
| | | |
| 1. Login Request | | |
|-------------------->| | |
| | | |
| 2. Redirect to Auth | |
| |--------------------->| |
| | | |
| 3. Login & Consent| | |
|<--------------------------------------------| |
| | | |
| 4. Auth Code | | |
|-------------------->|<---------------------| |
| | | |
| 5. Exchange Code for Token | |
| |--------------------->| |
| | 6. Access Token | |
| |<---------------------| |
| | | |
| 7. Access Resource | |
| |------------------------------------->| |
| | 8. Protected Resource | |
| |<-------------------------------------| |
Implementation Example (Node.js):
const express = require('express');
const axios = require('axios');
const app = express();
const config = {
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
redirectUri: 'http://localhost:3000/callback',
authorizationUrl: 'https://oauth.provider.com/authorize',
tokenUrl: 'https://oauth.provider.com/token',
scope: 'read:user'
};
// Step 1: Redirect to authorization server
app.get('/login', (req, res) => {
const params = new URLSearchParams({
response_type: 'code',
client_id: config.clientId,
redirect_uri: config.redirectUri,
scope: config.scope,
state: generateRandomState() // CSRF protection
});
res.redirect(`${config.authorizationUrl}?${params}`);
});
// Step 2: Handle callback with authorization code
app.get('/callback', async (req, res) => {
const { code, state } = req.query;
// Verify state parameter (CSRF protection)
if (!verifyState(state)) {
return res.status(400).send('Invalid state');
}
try {
// Step 3: Exchange code for access token
const response = await axios.post(config.tokenUrl, {
grant_type: 'authorization_code',
code,
client_id: config.clientId,
client_secret: config.clientSecret,
redirect_uri: config.redirectUri
});
const { access_token, refresh_token, expires_in } = response.data;
// Store tokens securely (session, database, etc.)
req.session.accessToken = access_token;
req.session.refreshToken = refresh_token;
res.redirect('/profile');
} catch (error) {
res.status(500).send('Authentication failed');
}
});
// Use access token to access protected resources
app.get('/profile', async (req, res) => {
const accessToken = req.session.accessToken;
if (!accessToken) {
return res.redirect('/login');
}
try {
const response = await axios.get('https://api.provider.com/user', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
res.json(response.data);
} catch (error) {
if (error.response?.status === 401) {
// Token expired, refresh it
return refreshAccessToken(req, res);
}
res.status(500).send('Error fetching profile');
}
});
// Refresh access token
async function refreshAccessToken(req, res) {
const refreshToken = req.session.refreshToken;
try {
const response = await axios.post(config.tokenUrl, {
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: config.clientId,
client_secret: config.clientSecret
});
req.session.accessToken = response.data.access_token;
res.redirect('/profile');
} catch (error) {
res.redirect('/login');
}
}
2. Authorization Code Flow with PKCE
Use Case: Mobile and single-page applications (no client secret)
const crypto = require('crypto');
// Generate code verifier and challenge
function generatePKCE() {
const codeVerifier = crypto.randomBytes(32).toString('base64url');
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url');
return { codeVerifier, codeChallenge };
}
// Step 1: Authorization request with PKCE
app.get('/login-pkce', (req, res) => {
const { codeVerifier, codeChallenge } = generatePKCE();
// Store code verifier for later use
req.session.codeVerifier = codeVerifier;
const params = new URLSearchParams({
response_type: 'code',
client_id: config.clientId,
redirect_uri: config.redirectUri,
scope: config.scope,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
state: generateRandomState()
});
res.redirect(`${config.authorizationUrl}?${params}`);
});
// Step 2: Exchange code with code verifier
app.get('/callback-pkce', async (req, res) => {
const { code } = req.query;
const codeVerifier = req.session.codeVerifier;
try {
const response = await axios.post(config.tokenUrl, {
grant_type: 'authorization_code',
code,
client_id: config.clientId,
redirect_uri: config.redirectUri,
code_verifier: codeVerifier
// No client_secret needed with PKCE
});
const { access_token } = response.data;
req.session.accessToken = access_token;
res.redirect('/profile');
} catch (error) {
res.status(500).send('Authentication failed');
}
});
3. Client Credentials Flow
Use Case: Machine-to-machine (M2M) communication
// No user involved, direct token request
async function getClientCredentialsToken() {
try {
const response = await axios.post(config.tokenUrl, {
grant_type: 'client_credentials',
client_id: config.clientId,
client_secret: config.clientSecret,
scope: 'api:read api:write'
}, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
return response.data.access_token;
} catch (error) {
console.error('Failed to get access token:', error);
throw error;
}
}
// Use token for API requests
async function callAPI() {
const token = await getClientCredentialsToken();
const response = await axios.get('https://api.example.com/data', {
headers: {
'Authorization': `Bearer ${token}`
}
});
return response.data;
}
4. Implicit Flow (Deprecated)
Not recommended - use Authorization Code Flow with PKCE instead
// ❌ BAD: Implicit flow (deprecated)
app.get('/login-implicit', (req, res) => {
const params = new URLSearchParams({
response_type: 'token', // Returns token in URL fragment
client_id: config.clientId,
redirect_uri: config.redirectUri,
scope: config.scope
});
res.redirect(`${config.authorizationUrl}?${params}`);
});
// Token exposed in URL - security risk!
5. Resource Owner Password Credentials (Legacy)
Not recommended - requires user to share password with client
// ❌ BAD: Password credentials (legacy)
async function loginWithPassword(username, password) {
const response = await axios.post(config.tokenUrl, {
grant_type: 'password',
username,
password,
client_id: config.clientId,
client_secret: config.clientSecret
});
return response.data.access_token;
}
OpenID Connect (OIDC)
OAuth 2.0 + Authentication Layer
ID Token (JWT)
// ID Token structure
{
"header": {
"alg": "RS256",
"kid": "key-id"
},
"payload": {
"iss": "https://accounts.google.com",
"sub": "user-unique-id",
"aud": "your-client-id",
"exp": 1234567890,
"iat": 1234567800,
"email": "user@example.com",
"email_verified": true,
"name": "John Doe",
"picture": "https://example.com/photo.jpg"
},
"signature": "..."
}
Verify ID Token
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
const client = jwksClient({
jwksUri: 'https://oauth.provider.com/.well-known/jwks.json'
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
const signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
});
}
function verifyIdToken(idToken) {
return new Promise((resolve, reject) => {
jwt.verify(idToken, getKey, {
algorithms: ['RS256'],
audience: config.clientId,
issuer: 'https://oauth.provider.com'
}, (err, decoded) => {
if (err) {
reject(err);
} else {
resolve(decoded);
}
});
});
}
// Usage
app.get('/callback-oidc', async (req, res) => {
const { code } = req.query;
// Exchange code for tokens
const response = await axios.post(config.tokenUrl, {
grant_type: 'authorization_code',
code,
client_id: config.clientId,
client_secret: config.clientSecret,
redirect_uri: config.redirectUri
});
const { access_token, id_token } = response.data;
// Verify ID token
try {
const claims = await verifyIdToken(id_token);
console.log('User:', claims);
req.session.user = claims;
res.redirect('/dashboard');
} catch (error) {
res.status(401).send('Invalid ID token');
}
});
UserInfo Endpoint
async function getUserInfo(accessToken) {
const response = await axios.get('https://oauth.provider.com/userinfo', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
return response.data;
}
Provider-Specific Examples
Google OAuth
const config = {
clientId: 'your-client-id.apps.googleusercontent.com',
clientSecret: 'your-client-secret',
redirectUri: 'http://localhost:3000/callback',
authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
tokenUrl: 'https://oauth2.googleapis.com/token',
scope: 'openid profile email'
};
app.get('/login/google', (req, res) => {
const params = new URLSearchParams({
response_type: 'code',
client_id: config.clientId,
redirect_uri: config.redirectUri,
scope: config.scope,
access_type: 'offline', // Get refresh token
prompt: 'consent'
});
res.redirect(`${config.authorizationUrl}?${params}`);
});
GitHub OAuth
const config = {
clientId: 'github-client-id',
clientSecret: 'github-client-secret',
redirectUri: 'http://localhost:3000/callback/github',
authorizationUrl: 'https://github.com/login/oauth/authorize',
tokenUrl: 'https://github.com/login/oauth/access_token',
scope: 'read:user user:email'
};
app.get('/login/github', (req, res) => {
const params = new URLSearchParams({
client_id: config.clientId,
redirect_uri: config.redirectUri,
scope: config.scope,
state: generateRandomState()
});
res.redirect(`${config.authorizationUrl}?${params}`);
});
app.get('/callback/github', async (req, res) => {
const { code } = req.query;
const response = await axios.post(config.tokenUrl, {
client_id: config.clientId,
client_secret: config.clientSecret,
code,
redirect_uri: config.redirectUri
}, {
headers: {
'Accept': 'application/json'
}
});
const { access_token } = response.data;
// Get user info
const userResponse = await axios.get('https://api.github.com/user', {
headers: {
'Authorization': `Bearer ${access_token}`
}
});
req.session.user = userResponse.data;
res.redirect('/dashboard');
});
Facebook OAuth
const config = {
clientId: 'facebook-app-id',
clientSecret: 'facebook-app-secret',
redirectUri: 'http://localhost:3000/callback/facebook',
authorizationUrl: 'https://www.facebook.com/v18.0/dialog/oauth',
tokenUrl: 'https://graph.facebook.com/v18.0/oauth/access_token',
scope: 'email public_profile'
};
Security Best Practices
1. State Parameter (CSRF Protection)
const crypto = require('crypto');
function generateState() {
const state = crypto.randomBytes(32).toString('hex');
return state;
}
app.get('/login', (req, res) => {
const state = generateState();
req.session.oauthState = state;
const params = new URLSearchParams({
response_type: 'code',
client_id: config.clientId,
redirect_uri: config.redirectUri,
scope: config.scope,
state // CSRF token
});
res.redirect(`${config.authorizationUrl}?${params}`);
});
app.get('/callback', (req, res) => {
const { state } = req.query;
// Verify state parameter
if (state !== req.session.oauthState) {
return res.status(400).send('Invalid state - CSRF attack detected');
}
delete req.session.oauthState;
// Continue with token exchange...
});
2. Secure Token Storage
// ❌ BAD: Store in localStorage (XSS vulnerable)
localStorage.setItem('access_token', token);
// ✅ GOOD: Store in httpOnly cookie (backend only)
res.cookie('access_token', token, {
httpOnly: true,
secure: true, // HTTPS only
sameSite: 'strict',
maxAge: 3600000 // 1 hour
});
// ✅ GOOD: Store in session (server-side)
req.session.accessToken = token;
// ✅ GOOD: Store in encrypted database
await db.tokens.create({
userId: user.id,
accessToken: encrypt(token),
expiresAt: new Date(Date.now() + 3600000)
});
3. Token Validation
async function validateAccessToken(token) {
// Introspection endpoint
const response = await axios.post('https://oauth.provider.com/introspect', {
token,
client_id: config.clientId,
client_secret: config.clientSecret
});
return response.data.active === true;
}
// Middleware
async function requireAuth(req, res, next) {
const token = req.session.accessToken;
if (!token) {
return res.status(401).send('Unauthorized');
}
const isValid = await validateAccessToken(token);
if (!isValid) {
return res.status(401).send('Invalid token');
}
next();
}
app.get('/protected', requireAuth, (req, res) => {
res.json({ message: 'Protected resource' });
});
4. Scope Validation
function hasScope(requiredScopes) {
return (req, res, next) => {
const tokenScopes = req.session.scopes || [];
const hasAllScopes = requiredScopes.every(scope =>
tokenScopes.includes(scope)
);
if (!hasAllScopes) {
return res.status(403).send('Insufficient permissions');
}
next();
};
}
app.get('/admin', requireAuth, hasScope(['admin:read']), (req, res) => {
res.json({ message: 'Admin resource' });
});
5. Token Expiration
function isTokenExpired(token) {
const decoded = jwt.decode(token);
if (!decoded || !decoded.exp) {
return true;
}
const now = Math.floor(Date.now() / 1000);
return decoded.exp < now;
}
async function getValidToken(req) {
let token = req.session.accessToken;
if (isTokenExpired(token)) {
// Refresh token
token = await refreshAccessToken(req.session.refreshToken);
req.session.accessToken = token;
}
return token;
}
Common Vulnerabilities
1. Authorization Code Interception
Risk: Attacker intercepts authorization code
Mitigation: Use PKCE
// Use PKCE for mobile and SPA apps
const { codeVerifier, codeChallenge } = generatePKCE();
2. Redirect URI Manipulation
Risk: Attacker redirects to malicious URL
Mitigation: Strict redirect URI validation
const ALLOWED_REDIRECTS = [
'http://localhost:3000/callback',
'https://myapp.com/callback'
];
function validateRedirectUri(uri) {
return ALLOWED_REDIRECTS.includes(uri);
}
3. Token Leakage
Risk: Tokens exposed in logs, URLs, browser history
Mitigation: Never expose tokens in URLs
// ❌ BAD: Token in URL
res.redirect(`/dashboard?token=${accessToken}`);
// ✅ GOOD: Token in secure cookie or session
req.session.accessToken = accessToken;
res.redirect('/dashboard');
Testing OAuth
Mock OAuth Server
// For testing purposes
app.get('/mock/authorize', (req, res) => {
const { redirect_uri, state } = req.query;
const code = 'mock-auth-code';
res.redirect(`${redirect_uri}?code=${code}&state=${state}`);
});
app.post('/mock/token', (req, res) => {
res.json({
access_token: 'mock-access-token',
token_type: 'Bearer',
expires_in: 3600,
refresh_token: 'mock-refresh-token'
});
});
Tips
OAuth vs OpenID Connect
| Feature | OAuth 2.0 | OpenID Connect |
|---|---|---|
| Purpose | Authorization | Authentication + Authorization |
| Token | Access Token | Access Token + ID Token |
| User Info | No standard | UserInfo endpoint |
| Use Case | API access | User login |
| Scope | Custom scopes | openid scope required |