Authentication and authorization are fundamental to application security. This guide covers JWT tokens, OAuth2, role-based access control, and best practices for implementing secure access controls.
JWT (JSON Web Tokens) Implementation
JWT Structure
JWTs consist of three parts: Header, Payload, and Signature, separated by dots.
Creating and Verifying JWTs
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET!;
const JWT_EXPIRES_IN = '15m';
const REFRESH_TOKEN_EXPIRES_IN = '7d';
// Create access token
function createAccessToken(userId: string, roles: string[]): string {
return jwt.sign(
{
sub: userId,
roles,
type: 'access',
},
JWT_SECRET,
{
expiresIn: JWT_EXPIRES_IN,
issuer: 'your-app',
audience: 'your-app-users',
}
);
}
// Create refresh token
function createRefreshToken(userId: string): string {
return jwt.sign(
{
sub: userId,
type: 'refresh',
},
JWT_SECRET,
{
expiresIn: REFRESH_TOKEN_EXPIRES_IN,
}
);
}
// Verify token
function verifyToken(token: string): any {
try {
return jwt.verify(token, JWT_SECRET, {
issuer: 'your-app',
audience: 'your-app-users',
});
} catch (error) {
throw new Error('Invalid token');
}
}
Secure Token Storage
// ✅ Store access token in memory (not localStorage)
// ✅ Store refresh token in httpOnly cookie
// Server-side cookie setting
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
});
// Client-side: Store access token in memory or sessionStorage
// Never in localStorage (vulnerable to XSS)
OAuth2 Implementation
Authorization Code Flow
// Step 1: Redirect to authorization server
const authUrl = new URL('https://oauth-provider.com/authorize');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'read write');
authUrl.searchParams.set('state', generateState()); // CSRF protection
res.redirect(authUrl.toString());
// Step 2: Handle callback
app.get('/oauth/callback', async (req, res) => {
const { code, state } = req.query;
// Verify state
if (state !== req.session.oauthState) {
return res.status(400).json({ error: 'Invalid state' });
}
// Exchange code for token
const tokenResponse = await fetch('https://oauth-provider.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code as string,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
}),
});
const tokens = await tokenResponse.json();
// Store tokens securely
});
Role-Based Access Control (RBAC)
RBAC Implementation
// Define roles and permissions
enum Role {
ADMIN = 'admin',
MODERATOR = 'moderator',
USER = 'user',
}
enum Permission {
READ_POSTS = 'read:posts',
WRITE_POSTS = 'write:posts',
DELETE_POSTS = 'delete:posts',
MANAGE_USERS = 'manage:users',
}
const rolePermissions: Record = {
[Role.ADMIN]: [
Permission.READ_POSTS,
Permission.WRITE_POSTS,
Permission.DELETE_POSTS,
Permission.MANAGE_USERS,
],
[Role.MODERATOR]: [
Permission.READ_POSTS,
Permission.WRITE_POSTS,
Permission.DELETE_POSTS,
],
[Role.USER]: [
Permission.READ_POSTS,
Permission.WRITE_POSTS,
],
};
// Middleware to check permissions
function requirePermission(permission: Permission) {
return (req: Request, res: Response, next: NextFunction) => {
const user = req.user;
const userPermissions = rolePermissions[user.role];
if (!userPermissions.includes(permission)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// Usage
app.delete('/posts/:id',
authenticate,
requirePermission(Permission.DELETE_POSTS),
deletePost
);
Session Management
Secure Session Configuration
import session from 'express-session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';
const redisClient = createClient({
url: process.env.REDIS_URL,
});
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET!,
name: 'sessionId',
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000, // 24 hours
},
}));
// Regenerate session ID after login
function regenerateSession(req: Request) {
return new Promise((resolve, reject) => {
req.session.regenerate((err) => {
if (err) reject(err);
else resolve(undefined);
});
});
}
Multi-Factor Authentication (MFA)
import speakeasy from 'speakeasy';
import QRCode from 'qrcode';
// Generate secret for user
function generateMFASecret(userId: string) {
const secret = speakeasy.generateSecret({
name: `Your App (${userId})`,
issuer: 'Your App',
});
// Store secret.uri in database
return secret;
}
// Verify TOTP code
function verifyMFA(userId: string, token: string): boolean {
const userSecret = getUserMFASecret(userId);
return speakeasy.totp.verify({
secret: userSecret,
encoding: 'base32',
token,
window: 2, // Allow 2 time steps tolerance
});
}
// Generate QR code
async function generateQRCode(secretUri: string): Promise {
return await QRCode.toDataURL(secretUri);
}
Password Reset Security
import crypto from 'crypto';
// Generate secure reset token
function generateResetToken(): string {
return crypto.randomBytes(32).toString('hex');
}
// Store token with expiration
async function createPasswordResetToken(userId: string) {
const token = generateResetToken();
const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
await db.passwordResetTokens.create({
data: {
userId,
token: await hashToken(token),
expiresAt,
},
});
return token;
}
// Verify and use token
async function resetPassword(token: string, newPassword: string) {
const hashedToken = await hashToken(token);
const resetToken = await db.passwordResetTokens.findUnique({
where: { token: hashedToken },
});
if (!resetToken || resetToken.expiresAt < new Date()) {
throw new Error('Invalid or expired token');
}
// Update password
await db.users.update({
where: { id: resetToken.userId },
data: { password: await hashPassword(newPassword) },
});
// Delete used token
await db.passwordResetTokens.delete({
where: { id: resetToken.id },
});
}
API Key Authentication
// Generate API key
function generateAPIKey(): string {
return crypto.randomBytes(32).toString('base64url');
}
// Store hashed API key
async function createAPIKey(userId: string, name: string) {
const apiKey = generateAPIKey();
const hashedKey = await hashAPIKey(apiKey);
await db.apiKeys.create({
data: {
userId,
name,
keyHash: hashedKey,
lastUsed: null,
},
});
// Return plain key only once
return apiKey;
}
// Authenticate with API key
async function authenticateAPIKey(key: string) {
const hashedKey = await hashAPIKey(key);
const apiKey = await db.apiKeys.findUnique({
where: { keyHash: hashedKey },
include: { user: true },
});
if (!apiKey) {
throw new Error('Invalid API key');
}
// Update last used
await db.apiKeys.update({
where: { id: apiKey.id },
data: { lastUsed: new Date() },
});
return apiKey.user;
}
Best Practices
- Use short-lived access tokens (15 minutes)
- Implement refresh token rotation
- Store tokens securely (httpOnly cookies for refresh tokens)
- Implement token blacklisting for logout
- Use strong, unique secrets for JWT signing
- Validate tokens on every request
- Implement rate limiting on authentication endpoints
- Log authentication events for security monitoring
- Require MFA for sensitive operations
- Implement account lockout after failed attempts
Conclusion
Secure authentication and authorization are critical for application security. Use JWTs for stateless authentication, implement OAuth2 for third-party integration, and use RBAC for fine-grained access control. Always follow security best practices: use short token lifetimes, implement refresh token rotation, store tokens securely, and monitor authentication events.