Authentication and Authorization Design Patterns - sonuprajapati15/Authentication-Authorization GitHub Wiki
This document provides a solution for handling authentication and authorization, including token expiration and refresh strategies. It describes various authentication and authorization design patterns, including:
- Token-Based Authentication
- Session-Based Authentication
- OAuth 2.0 Authentication
- SAML Authentication
- Multi-Factor Authentication (MFA)
- Role-Based Access Control (RBAC)
- Attribute-Based Access Control (ABAC)
- Zero Trust Security Model
- Uses JWT (JSON Web Tokens) for stateless authentication.
- Tokens are stored in localStorage, sessionStorage, or HttpOnly cookies.
- Suitable for single-page applications (SPAs) and mobile apps.
- Traditional approach using server-side sessions.
- Sessions are stored in databases or in-memory caches (e.g., Redis).
- Best suited for monolithic applications.
- Industry-standard protocol for authorization.
- Uses access tokens and refresh tokens.
- Supports third-party authentication via providers like Google, Facebook.
- Security Assertion Markup Language (SAML) is used for Single Sign-On (SSO).
- Common in enterprise applications for federated authentication.
- Enhances security by requiring multiple verification factors.
- Examples: SMS OTP, email codes, biometric authentication.
- Users are assigned predefined roles (e.g., Admin, Editor, Viewer).
- Access permissions are granted based on roles.
- Simple and widely used in enterprise applications.
- Access is granted based on user attributes (e.g., department, location).
- Provides dynamic and fine-grained access control.
- "Never trust, always verify" approach.
- Requires authentication and authorization at every access request.
- Used in highly secure environments.
- Secure authentication and authorization for modern applications.
- Ensure minimal user friction while maintaining security.
- Choose the appropriate authentication mechanism based on application needs.
- Implement secure authorization patterns to restrict access.
graph TD;
User -->|Login| AuthServer;
AuthServer -->|Generate Access & Refresh Token| User;
User -->|Send Access Token| API;
API -->|Validate Token| AuthServer;
AuthServer -->|Token Expired?| CheckExpiration;
CheckExpiration -->|Yes| RefreshToken;
RefreshToken -->|Return New Access Token| API;
API -->|Serve Request| User;
- The client checks token validity before making API calls.
- Refreshes the token when necessary.
import axios from 'axios';
const api = axios.create({
baseURL: 'https://your-api.com',
withCredentials: true,
});
api.interceptors.request.use(async (config) => {
let accessToken = localStorage.getItem('accessToken');
if (isTokenExpired(accessToken)) {
accessToken = await refreshToken();
}
config.headers.Authorization = `Bearer ${accessToken}`;
return config;
}, (error) => Promise.reject(error));
async function refreshToken() {
try {
const response = await axios.post('https://your-api.com/refresh-token', {}, { withCredentials: true });
localStorage.setItem('accessToken', response.data.accessToken);
return response.data.accessToken;
} catch (error) {
console.error('Session expired. Please log in again.');
}
}
- The backend checks token validity before processing API requests.
- Issues a new token if the existing one is expired.
app.use(async (req, res, next) => {
let accessToken = req.headers.authorization?.split(" ")[1];
if (!accessToken) return res.sendStatus(401);
try {
jwt.verify(accessToken, ACCESS_SECRET);
return next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) return res.sendStatus(403);
try {
const payload = jwt.verify(refreshToken, REFRESH_SECRET);
const newAccessToken = jwt.sign({ userId: payload.userId }, ACCESS_SECRET, { expiresIn: '15m' });
res.setHeader('Authorization', `Bearer ${newAccessToken}`);
req.headers.authorization = `Bearer ${newAccessToken}`;
return next();
} catch (refreshErr) {
return res.sendStatus(403);
}
} else {
return res.sendStatus(401);
}
}
});
- Choose an authentication pattern based on security and user experience needs.
- Implement authorization models (RBAC, ABAC) based on access control requirements.
- Always store refresh tokens securely using HttpOnly cookies.
- Implement OAuth 2.0 with PKCE for enhanced security.
- Use adaptive authentication based on user behavior.
- Apply fine-grained access control using ABAC models.
There are two main strategies for handling token refresh, each with its own pros and cons:
The client checks token validity and refreshes it before making API calls.
- Before every API call, the client checks if the access token is expired.
- If expired, it calls the refresh token API to get a new access token.
- Stores the new access token and proceeds with the original API request.
-
Store Tokens Securely:
- Store the access token in memory (or localStorage if necessary).
- Store the refresh token in HttpOnly Secure Cookies (so it's not accessible via JavaScript).
-
Intercept Requests (Axios Example):
- Use an Axios interceptor (or similar for Fetch API) to check token validity before each request.
import axios from 'axios';
// Create Axios instance
const api = axios.create({
baseURL: 'https://your-api.com',
withCredentials: true, // Important for sending cookies
});
// Request interceptor for automatic refresh
api.interceptors.request.use(
async (config) => {
let accessToken = localStorage.getItem('accessToken');
// Check if token is expired
if (isTokenExpired(accessToken)) {
accessToken = await refreshToken();
}
// Attach new access token to request
config.headers.Authorization = `Bearer ${accessToken}`;
return config;
},
(error) => Promise.reject(error)
);
// Function to refresh token
async function refreshToken() {
try {
const response = await axios.post('https://your-api.com/refresh-token', {}, { withCredentials: true });
const { accessToken } = response.data;
localStorage.setItem('accessToken', accessToken);
return accessToken;
} catch (error) {
console.error('Session expired. Please log in again.');
// Redirect to login
}
}
// Function to check if token is expired
function isTokenExpired(token) {
if (!token) return true;
const { exp } = JSON.parse(atob(token.split('.')[1]));
return exp < Date.now() / 1000;
}
export default api;
- Lower backend load (tokens are refreshed only when needed).
- Faster responses as API calls donβt wait for the server to refresh tokens.
- Works well for frontend-heavy applications.
- Relies on client storage, which is less secure than backend-managed solutions.
- More logic on the client side.
The backend checks the token's validity and refreshes before every API response.
- The client sends the access token in the request.
- The backend:
- Validates the access token.
- If expired, uses the refresh token (sent via cookies) to get a new access token.
- Sends the new access + refresh token in the API response.
- The client updates the stored tokens.
- Client sends access token (or gets auto-refreshed)
- Backend verifies token & refreshes if expired
- Backend responds with new access token (if refreshed)
app.use(async (req, res, next) => {
let accessToken = req.headers.authorization?.split(" ")[1];
if (!accessToken) return res.sendStatus(401);
try {
// Verify access token
jwt.verify(accessToken, ACCESS_SECRET);
return next(); // Token valid, proceed with API request
} catch (err) {
if (err.name === 'TokenExpiredError') {
// Try to refresh token
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) return res.sendStatus(403); // No refresh token, force login
try {
const payload = jwt.verify(refreshToken, REFRESH_SECRET);
const newAccessToken = jwt.sign({ userId: payload.userId }, ACCESS_SECRET, { expiresIn: '15m' });
// Send new token in response headers
res.setHeader('Authorization', `Bearer ${newAccessToken}`);
req.headers.authorization = `Bearer ${newAccessToken}`; // Set for next middleware
return next();
} catch (refreshErr) {
return res.sendStatus(403); // Refresh token invalid, force login
}
} else {
return res.sendStatus(401);
}
}
});
- More secure (refresh logic is handled on the server).
- Client doesnβt have to manage token refresh logic.
- Works well for backend-heavy applications or APIs.
- Every API call might involve token refresh (adds latency).
- Increases backend load due to frequent refresh token checks.
Feature | Client-Managed Refresh | Backend-Managed Refresh |
---|---|---|
Security | πΈ Moderate (tokens stored on client) | β High (tokens refreshed on backend) |
Performance | β Faster (refresh only when needed) | πΈ Slightly slower (refresh checked in backend) |
Backend Load | β Lower load | πΈ Higher load |
Works for Web Apps | β Yes | β Yes |
Works for APIs | πΈ Requires client handling | β Fully managed |
Best for... | Frontend-heavy apps | Secure API systems |
- If you want better security, go for backend-managed.
- If you need better performance, use client-managed with refresh before API calls.