Authentication and Authorization are the foundation of application security. This article explains the design and implementation of authentication and authorization patterns in modern web applications.
Basics of Authentication and Authorization
Conceptual Differences
Authentication - “Who are you?”
sequenceDiagram
participant User
participant System
User->>System: Present Credentials (ID/Password)
System->>User: Identity Result (Auth Token)
Authorization - “What can you do?”
sequenceDiagram
participant User
participant System
User->>System: Request Resource (+ Auth Token)
System->>System: Check Permissions
System->>User: Allow/Deny
Comparison of Authentication Methods
| Method | Characteristics | Use Cases |
|---|---|---|
| Session-based | Server-side state management | Traditional web apps |
| JWT (Stateless) | Token contains information | SPA, API |
| OAuth 2.0 / OIDC | External auth provider integration | Social login |
| API Key | Static key | Server-to-server communication |
| mTLS | Certificate-based | Between microservices |
Session-Based Authentication
Implementation Example
// session-auth.ts
import { randomBytes, createHash } from 'crypto';
import Redis from 'ioredis';
interface Session {
userId: string;
email: string;
role: string;
createdAt: number;
lastAccessedAt: number;
userAgent: string;
ip: string;
}
class SessionManager {
private redis: Redis;
private readonly SESSION_TTL = 24 * 60 * 60; // 24 hours
private readonly SESSION_PREFIX = 'session:';
constructor(redisUrl: string) {
this.redis = new Redis(redisUrl);
}
async createSession(userId: string, userData: Partial<Session>, req: Request): Promise<string> {
const sessionId = this.generateSessionId();
const hashedId = this.hashSessionId(sessionId);
const session: Session = {
userId,
email: userData.email || '',
role: userData.role || 'user',
createdAt: Date.now(),
lastAccessedAt: Date.now(),
userAgent: req.headers.get('user-agent') || '',
ip: req.headers.get('x-forwarded-for') || '',
};
await this.redis.setex(
`${this.SESSION_PREFIX}${hashedId}`,
this.SESSION_TTL,
JSON.stringify(session)
);
// Manage list of user sessions
await this.redis.sadd(`user_sessions:${userId}`, hashedId);
return sessionId;
}
async getSession(sessionId: string): Promise<Session | null> {
const hashedId = this.hashSessionId(sessionId);
const data = await this.redis.get(`${this.SESSION_PREFIX}${hashedId}`);
if (!data) return null;
const session: Session = JSON.parse(data);
// Update last access time
session.lastAccessedAt = Date.now();
await this.redis.setex(
`${this.SESSION_PREFIX}${hashedId}`,
this.SESSION_TTL,
JSON.stringify(session)
);
return session;
}
async destroySession(sessionId: string): Promise<void> {
const hashedId = this.hashSessionId(sessionId);
const data = await this.redis.get(`${this.SESSION_PREFIX}${hashedId}`);
if (data) {
const session: Session = JSON.parse(data);
await this.redis.srem(`user_sessions:${session.userId}`, hashedId);
}
await this.redis.del(`${this.SESSION_PREFIX}${hashedId}`);
}
async destroyAllUserSessions(userId: string): Promise<void> {
const sessionIds = await this.redis.smembers(`user_sessions:${userId}`);
for (const hashedId of sessionIds) {
await this.redis.del(`${this.SESSION_PREFIX}${hashedId}`);
}
await this.redis.del(`user_sessions:${userId}`);
}
private generateSessionId(): string {
return randomBytes(32).toString('hex');
}
private hashSessionId(sessionId: string): string {
return createHash('sha256').update(sessionId).digest('hex');
}
}
// Cookie configuration
function setSessionCookie(response: Response, sessionId: string): Response {
const cookie = [
`session=${sessionId}`,
'HttpOnly',
'Secure',
'SameSite=Strict',
'Path=/',
`Max-Age=${24 * 60 * 60}`,
].join('; ');
response.headers.set('Set-Cookie', cookie);
return response;
}
JWT-Based Authentication
JWT Implementation
// jwt-auth.ts
import { SignJWT, jwtVerify, JWTPayload } from 'jose';
interface TokenPayload extends JWTPayload {
sub: string; // userId
email: string;
role: string;
type: 'access' | 'refresh';
}
interface TokenPair {
accessToken: string;
refreshToken: string;
}
class JWTManager {
private readonly accessSecret: Uint8Array;
private readonly refreshSecret: Uint8Array;
private readonly issuer = 'myapp';
private readonly audience = 'myapp-users';
private readonly ACCESS_TOKEN_TTL = '15m';
private readonly REFRESH_TOKEN_TTL = '7d';
constructor(accessSecret: string, refreshSecret: string) {
this.accessSecret = new TextEncoder().encode(accessSecret);
this.refreshSecret = new TextEncoder().encode(refreshSecret);
}
async generateTokenPair(userId: string, userData: { email: string; role: string }): Promise<TokenPair> {
const now = new Date();
const accessToken = await new SignJWT({
sub: userId,
email: userData.email,
role: userData.role,
type: 'access',
})
.setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
.setIssuedAt()
.setIssuer(this.issuer)
.setAudience(this.audience)
.setExpirationTime(this.ACCESS_TOKEN_TTL)
.sign(this.accessSecret);
const refreshToken = await new SignJWT({
sub: userId,
type: 'refresh',
})
.setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
.setIssuedAt()
.setIssuer(this.issuer)
.setAudience(this.audience)
.setExpirationTime(this.REFRESH_TOKEN_TTL)
.sign(this.refreshSecret);
return { accessToken, refreshToken };
}
async verifyAccessToken(token: string): Promise<TokenPayload | null> {
try {
const { payload } = await jwtVerify(token, this.accessSecret, {
issuer: this.issuer,
audience: this.audience,
});
if (payload.type !== 'access') {
return null;
}
return payload as TokenPayload;
} catch (error) {
return null;
}
}
async verifyRefreshToken(token: string): Promise<TokenPayload | null> {
try {
const { payload } = await jwtVerify(token, this.refreshSecret, {
issuer: this.issuer,
audience: this.audience,
});
if (payload.type !== 'refresh') {
return null;
}
return payload as TokenPayload;
} catch (error) {
return null;
}
}
async refreshTokens(
refreshToken: string,
getUserData: (userId: string) => Promise<{ email: string; role: string } | null>
): Promise<TokenPair | null> {
const payload = await this.verifyRefreshToken(refreshToken);
if (!payload || !payload.sub) {
return null;
}
const userData = await getUserData(payload.sub);
if (!userData) {
return null;
}
return this.generateTokenPair(payload.sub, userData);
}
}
// Middleware
async function authMiddleware(
request: Request,
jwtManager: JWTManager
): Promise<TokenPayload | Response> {
const authHeader = request.headers.get('Authorization');
if (!authHeader?.startsWith('Bearer ')) {
return new Response('Unauthorized', { status: 401 });
}
const token = authHeader.slice(7);
const payload = await jwtManager.verifyAccessToken(token);
if (!payload) {
return new Response('Invalid token', { status: 401 });
}
return payload;
}
Token Refresh Flow
sequenceDiagram
participant Client
participant Server
Note over Client, Server: 1. Initial Login
Client->>Server: POST /auth/login<br/>{email, password}
Server->>Client: 200 OK<br/>{accessToken, refreshToken}
Note over Client, Server: 2. API Call
Client->>Server: GET /api/resource<br/>Authorization: Bearer {accessToken}
Server->>Client: 200 OK / 401 Unauthorized
Note over Client, Server: 3. Token Refresh (when accessToken expires)
Client->>Server: POST /auth/refresh<br/>{refreshToken}
Server->>Client: 200 OK<br/>{accessToken, refreshToken} (both new)
Authorization Patterns
RBAC (Role-Based Access Control)
// rbac.ts
type Permission =
| 'read:users' | 'write:users' | 'delete:users'
| 'read:posts' | 'write:posts' | 'delete:posts'
| 'admin:*';
type Role = 'guest' | 'user' | 'moderator' | 'admin';
const rolePermissions: Record<Role, Permission[]> = {
guest: ['read:posts'],
user: ['read:posts', 'write:posts', 'read:users'],
moderator: ['read:posts', 'write:posts', 'delete:posts', 'read:users'],
admin: ['admin:*'],
};
class RBACAuthorizer {
hasPermission(role: Role, permission: Permission): boolean {
const permissions = rolePermissions[role];
if (permissions.includes('admin:*')) {
return true;
}
return permissions.includes(permission);
}
hasAnyPermission(role: Role, permissions: Permission[]): boolean {
return permissions.some(p => this.hasPermission(role, p));
}
hasAllPermissions(role: Role, permissions: Permission[]): boolean {
return permissions.every(p => this.hasPermission(role, p));
}
}
// Middleware
function requirePermission(...permissions: Permission[]) {
return async function (request: Request, user: TokenPayload): Promise<Response | null> {
const authorizer = new RBACAuthorizer();
if (!authorizer.hasAllPermissions(user.role as Role, permissions)) {
return new Response('Forbidden', { status: 403 });
}
return null; // Allowed
};
}
// Usage example
app.delete('/api/posts/:id', async (req) => {
const user = await authMiddleware(req, jwtManager);
if (user instanceof Response) return user;
const forbidden = await requirePermission('delete:posts')(req, user);
if (forbidden) return forbidden;
// Delete processing...
});
ABAC (Attribute-Based Access Control)
// abac.ts
interface Resource {
type: string;
ownerId: string;
attributes: Record<string, any>;
}
interface Subject {
id: string;
role: string;
department: string;
attributes: Record<string, any>;
}
interface Context {
time: Date;
ip: string;
location?: string;
}
type PolicyCondition = (subject: Subject, resource: Resource, context: Context) => boolean;
interface Policy {
name: string;
effect: 'allow' | 'deny';
actions: string[];
resources: string[];
condition: PolicyCondition;
}
class ABACAuthorizer {
private policies: Policy[] = [];
addPolicy(policy: Policy): void {
this.policies.push(policy);
}
evaluate(
action: string,
subject: Subject,
resource: Resource,
context: Context
): boolean {
// Search for matching policies
const matchingPolicies = this.policies.filter(policy => {
const actionMatch = policy.actions.includes(action) || policy.actions.includes('*');
const resourceMatch = policy.resources.includes(resource.type) || policy.resources.includes('*');
return actionMatch && resourceMatch;
});
// Default deny
if (matchingPolicies.length === 0) {
return false;
}
// Evaluate conditions
for (const policy of matchingPolicies) {
const conditionResult = policy.condition(subject, resource, context);
if (policy.effect === 'deny' && conditionResult) {
return false;
}
if (policy.effect === 'allow' && conditionResult) {
return true;
}
}
return false;
}
}
// Policy definitions
const authorizer = new ABACAuthorizer();
// Owner can edit their own resources
authorizer.addPolicy({
name: 'owner-can-edit',
effect: 'allow',
actions: ['update', 'delete'],
resources: ['post', 'comment'],
condition: (subject, resource) => subject.id === resource.ownerId,
});
// Admin has full access
authorizer.addPolicy({
name: 'admin-full-access',
effect: 'allow',
actions: ['*'],
resources: ['*'],
condition: (subject) => subject.role === 'admin',
});
// Can view resources from same department
authorizer.addPolicy({
name: 'same-department-read',
effect: 'allow',
actions: ['read'],
resources: ['document'],
condition: (subject, resource) =>
subject.department === resource.attributes.department,
});
// Restrict access outside business hours
authorizer.addPolicy({
name: 'business-hours-only',
effect: 'deny',
actions: ['*'],
resources: ['sensitive-data'],
condition: (_, __, context) => {
const hour = context.time.getHours();
return hour < 9 || hour >= 18; // Deny outside 9-18
},
});
Resource-Based Access Control
// resource-based-auth.ts
interface Post {
id: string;
authorId: string;
status: 'draft' | 'published' | 'archived';
visibility: 'public' | 'private' | 'members-only';
}
class PostAuthorizer {
canRead(user: Subject | null, post: Post): boolean {
// Public published posts are viewable by anyone
if (post.status === 'published' && post.visibility === 'public') {
return true;
}
// Unauthenticated users can only view public posts
if (!user) {
return false;
}
// Own posts are always viewable
if (post.authorId === user.id) {
return true;
}
// Admins can view everything
if (user.role === 'admin' || user.role === 'moderator') {
return true;
}
// Members-only posts
if (post.visibility === 'members-only' && post.status === 'published') {
return true;
}
return false;
}
canUpdate(user: Subject, post: Post): boolean {
// Only author can edit
if (post.authorId === user.id) {
return true;
}
// Admin can also edit
if (user.role === 'admin') {
return true;
}
return false;
}
canDelete(user: Subject, post: Post): boolean {
// Only author can delete (drafts only)
if (post.authorId === user.id && post.status === 'draft') {
return true;
}
// Admin can delete anything
if (user.role === 'admin') {
return true;
}
return false;
}
canPublish(user: Subject, post: Post): boolean {
// Only author can publish
if (post.authorId === user.id) {
return true;
}
// Admin can also publish
if (user.role === 'admin') {
return true;
}
return false;
}
}
// Usage example
const postAuth = new PostAuthorizer();
app.get('/api/posts/:id', async (req, { params }) => {
const user = await getOptionalUser(req); // May be null
const post = await getPost(params.id);
if (!post) {
return new Response('Not Found', { status: 404 });
}
if (!postAuth.canRead(user, post)) {
return new Response('Forbidden', { status: 403 });
}
return Response.json(post);
});
Multi-Factor Authentication (MFA)
TOTP Implementation
// mfa.ts
import { createHmac, randomBytes } from 'crypto';
class TOTPManager {
private readonly DIGITS = 6;
private readonly PERIOD = 30; // seconds
private readonly ALGORITHM = 'sha1';
generateSecret(): string {
return randomBytes(20).toString('base32').slice(0, 16);
}
generateOTP(secret: string, counter?: number): string {
const time = counter ?? Math.floor(Date.now() / 1000 / this.PERIOD);
const timeBuffer = Buffer.alloc(8);
timeBuffer.writeBigInt64BE(BigInt(time));
const decodedSecret = this.base32Decode(secret);
const hmac = createHmac(this.ALGORITHM, decodedSecret)
.update(timeBuffer)
.digest();
const offset = hmac[hmac.length - 1] & 0xf;
const binary =
((hmac[offset] & 0x7f) << 24) |
((hmac[offset + 1] & 0xff) << 16) |
((hmac[offset + 2] & 0xff) << 8) |
(hmac[offset + 3] & 0xff);
const otp = binary % Math.pow(10, this.DIGITS);
return otp.toString().padStart(this.DIGITS, '0');
}
verifyOTP(secret: string, otp: string, window: number = 1): boolean {
const currentCounter = Math.floor(Date.now() / 1000 / this.PERIOD);
for (let i = -window; i <= window; i++) {
const expectedOTP = this.generateOTP(secret, currentCounter + i);
if (this.timingSafeEqual(otp, expectedOTP)) {
return true;
}
}
return false;
}
generateQRCodeURL(secret: string, email: string, issuer: string): string {
const encodedIssuer = encodeURIComponent(issuer);
const encodedEmail = encodeURIComponent(email);
return `otpauth://totp/${encodedIssuer}:${encodedEmail}?secret=${secret}&issuer=${encodedIssuer}&algorithm=SHA1&digits=${this.DIGITS}&period=${this.PERIOD}`;
}
private base32Decode(input: string): Buffer {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
let bits = '';
for (const char of input.toUpperCase()) {
const val = alphabet.indexOf(char);
if (val === -1) continue;
bits += val.toString(2).padStart(5, '0');
}
const bytes: number[] = [];
for (let i = 0; i + 8 <= bits.length; i += 8) {
bytes.push(parseInt(bits.slice(i, i + 8), 2));
}
return Buffer.from(bytes);
}
private timingSafeEqual(a: string, b: string): boolean {
if (a.length !== b.length) return false;
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return result === 0;
}
}
// Backup codes
class BackupCodeManager {
generateCodes(count: number = 10): string[] {
const codes: string[] = [];
for (let i = 0; i < count; i++) {
const code = randomBytes(4).toString('hex').toUpperCase();
codes.push(`${code.slice(0, 4)}-${code.slice(4)}`);
}
return codes;
}
hashCode(code: string): string {
return createHash('sha256')
.update(code.replace('-', ''))
.digest('hex');
}
}
Security Best Practices
Password Policy
// password-policy.ts
interface PasswordValidationResult {
valid: boolean;
errors: string[];
strength: 'weak' | 'medium' | 'strong';
}
function validatePassword(password: string): PasswordValidationResult {
const errors: string[] = [];
// Length check
if (password.length < 8) {
errors.push('Password must be at least 8 characters');
}
if (password.length > 128) {
errors.push('Password must be 128 characters or less');
}
// Complexity check
if (!/[a-z]/.test(password)) {
errors.push('Must include lowercase letters');
}
if (!/[A-Z]/.test(password)) {
errors.push('Must include uppercase letters');
}
if (!/[0-9]/.test(password)) {
errors.push('Must include numbers');
}
if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
errors.push('Must include symbols');
}
// Common password check
const commonPasswords = ['password', '123456', 'qwerty', 'admin'];
if (commonPasswords.some(p => password.toLowerCase().includes(p))) {
errors.push('Password is too common');
}
// Strength calculation
let strength: 'weak' | 'medium' | 'strong' = 'weak';
if (errors.length === 0) {
if (password.length >= 12 && /[!@#$%^&*(),.?":{}|<>]/.test(password)) {
strength = 'strong';
} else {
strength = 'medium';
}
}
return {
valid: errors.length === 0,
errors,
strength,
};
}
Rate Limiting
// rate-limiter.ts
class RateLimiter {
private requests: Map<string, number[]> = new Map();
constructor(
private maxRequests: number,
private windowMs: number
) {}
isAllowed(key: string): boolean {
const now = Date.now();
const windowStart = now - this.windowMs;
let timestamps = this.requests.get(key) || [];
// Remove old requests
timestamps = timestamps.filter(t => t > windowStart);
if (timestamps.length >= this.maxRequests) {
return false;
}
timestamps.push(now);
this.requests.set(key, timestamps);
return true;
}
getRemainingRequests(key: string): number {
const now = Date.now();
const windowStart = now - this.windowMs;
const timestamps = (this.requests.get(key) || []).filter(t => t > windowStart);
return Math.max(0, this.maxRequests - timestamps.length);
}
}
// Login attempt limiting
const loginLimiter = new RateLimiter(5, 15 * 60 * 1000); // 5 times per 15 minutes
app.post('/auth/login', async (req) => {
const ip = req.headers.get('x-forwarded-for') || 'unknown';
if (!loginLimiter.isAllowed(ip)) {
return new Response('Too many login attempts', {
status: 429,
headers: {
'Retry-After': '900', // 15 minutes
},
});
}
// Login processing...
});
Summary
Authentication and authorization are the cornerstone of application security.
Selection Guidelines
| Requirement | Recommended Pattern |
|---|---|
| Traditional web apps | Session + RBAC |
| SPA/Mobile | JWT + OAuth 2.0 |
| Microservices | JWT + mTLS |
| Complex permissions | ABAC |
| Simple permissions | RBAC |
Security Checklist
- Passwords: Proper hashing (Argon2id/bcrypt)
- Tokens: Appropriate expiration and renewal
- Sessions: HttpOnly, Secure, SameSite Cookie
- MFA: Required for critical operations
- Rate Limiting: Brute force protection
- Audit Logs: Record authentication events
Proper implementation of authentication and authorization enables building secure applications.