Complete Guide to Authentication and Authorization Patterns - Designing Secure Access Control

2025.12.02

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

MethodCharacteristicsUse Cases
Session-basedServer-side state managementTraditional web apps
JWT (Stateless)Token contains informationSPA, API
OAuth 2.0 / OIDCExternal auth provider integrationSocial login
API KeyStatic keyServer-to-server communication
mTLSCertificate-basedBetween 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

RequirementRecommended Pattern
Traditional web appsSession + RBAC
SPA/MobileJWT + OAuth 2.0
MicroservicesJWT + mTLS
Complex permissionsABAC
Simple permissionsRBAC

Security Checklist

  1. Passwords: Proper hashing (Argon2id/bcrypt)
  2. Tokens: Appropriate expiration and renewal
  3. Sessions: HttpOnly, Secure, SameSite Cookie
  4. MFA: Required for critical operations
  5. Rate Limiting: Brute force protection
  6. Audit Logs: Record authentication events

Proper implementation of authentication and authorization enables building secure applications.

References

← Back to list