Shortcuts
BlogNovember 10, 2024

Mohamed Elbarry
Authentication and Authorization
Authentication answers “who are you?”; authorization answers “what can you do?” I’ve seen apps that conflate the two—e.g. checking “is admin” in the UI but not on the API—and end up with holes. Here’s how I structure both so they stay consistent and auditable. On projects like Lumin AI we needed auth and real-time together. I never store plain-text passwords. bcrypt with a cost of 12 is my default; each step up doubles the work, and 12 gets you into the 100–300ms range on typical hardware—slow enough to throttle brute force, fast enough that login still feels fine. OWASP recommends at least 10 for bcrypt; PHP moved its default from 10 to 12 for the same reason. For new greenfield work OWASP now prefers Argon2id over bcrypt when your stack supports it. I hash on signup and compare on login, and never log or return the hash.
Typescript
import bcrypt from 'bcrypt';

const SALT_ROUNDS = 12;

export class AuthService {
  async hashPassword(password: string): Promise<string> {
    return await bcrypt.hash(password, SALT_ROUNDS);
  }

  async verifyPassword(password: string, hashedPassword: string): Promise<boolean> {
    return await bcrypt.compare(password, hashedPassword);
  }

  async createUser(userData: CreateUserData): Promise<User> {
    const hashedPassword = await this.hashPassword(userData.password);

    const user = await this.userRepository.create({
      ...userData,
      password: hashedPassword,
    });

    return user;
  }
}
For APIs that don’t want server-side sessions I use JWTs: sign on login, verify on each request, and keep the payload small (e.g. userId, role). Short expiry (e.g. 24h) and refresh tokens for long-lived sessions reduce the blast radius if a token leaks.
Typescript
import jwt from 'jsonwebtoken';

interface TokenPayload {
  userId: string;
  email: string;
  role: string;
}

export class JWTService {
  private readonly secret: string;
  private readonly expiresIn: string;

  constructor() {
    this.secret = process.env.JWT_SECRET!;
    this.expiresIn = process.env.JWT_EXPIRES_IN || '24h';
  }

  generateToken(payload: TokenPayload): string {
    return jwt.sign(payload, this.secret, {
      expiresIn: this.expiresIn,
      issuer: 'myapp.com',
      audience: 'myapp-users',
    });
  }

  verifyToken(token: string): TokenPayload | null {
    try {
      const decoded = jwt.verify(token, this.secret) as TokenPayload;
      return decoded;
    } catch (error) {
      return null;
    }
  }
}
When I need revocable sessions or server-side state I use sessions with a store (e.g. Redis). Cookie is httpOnly, secure in production, sameSite strict. That way the browser sends the session id and I can invalidate it on logout or after a password change.
Typescript
import session from 'express-session';
import connectRedis from 'connect-redis';
import Redis from 'ioredis';

const RedisStore = connectRedis(session);
const redis = new Redis(process.env.REDIS_URL);

export const sessionConfig = {
  store: new RedisStore({ client: redis }),
  secret: process.env.SESSION_SECRET!,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    maxAge: 24 * 60 * 60 * 1000, // 24 hours
    sameSite: 'strict' as const,
  },
  name: 'sessionId',
};
For “Sign in with Google” I use the official client, verify the id token on the backend, and then create or link a user. I don’t trust the client for identity—only the token from the provider.
Typescript
import { OAuth2Client } from 'google-auth-library';

export class GoogleAuthService {
  private client: OAuth2Client;

  constructor() {
    this.client = new OAuth2Client(
      process.env.GOOGLE_CLIENT_ID,
      process.env.GOOGLE_CLIENT_SECRET,
      process.env.GOOGLE_REDIRECT_URI
    );
  }

  getAuthUrl(): string {
    return this.client.generateAuthUrl({
      access_type: 'offline',
      scope: ['profile', 'email'],
      prompt: 'consent',
    });
  }

  async getUserInfo(accessToken: string): Promise<GoogleUserInfo> {
    const ticket = await this.client.verifyIdToken({
      idToken: accessToken,
      audience: process.env.GOOGLE_CLIENT_ID,
    });

    const payload = ticket.getPayload();
    return {
      id: payload!.sub,
      email: payload!.email!,
      name: payload!.name!,
      picture: payload!.picture,
    };
  }
}
For sensitive apps I add TOTP as a second factor. I generate a secret, show a QR for the user’s authenticator app, and verify the code on login with a small time window for clock skew.
Typescript
import speakeasy from 'speakeasy';
import QRCode from 'qrcode';

export class MFAService {
  generateSecret(userEmail: string): { secret: string; qrCodeUrl: string } {
    const secret = speakeasy.generateSecret({
      name: `MyApp (${userEmail})`,
      issuer: 'MyApp',
    });

    return {
      secret: secret.base32,
      qrCodeUrl: secret.otpauth_url!,
    };
  }

  verifyTOTP(token: string, secret: string): boolean {
    return speakeasy.totp.verify({
      secret,
      encoding: 'base32',
      token,
      window: 2,
    });
  }
}
I model permissions as roles (e.g. admin, moderator, user) and check the role (or a permission derived from it) on the server for every protected action. The UI can hide buttons, but the API must enforce.
Typescript
enum Role {
  ADMIN = 'admin',
  MODERATOR = 'moderator',
  USER = 'user',
  GUEST = 'guest',
}

enum Permission {
  READ_USERS = 'read:users',
  WRITE_USERS = 'write:users',
  DELETE_USERS = 'delete:users',
  READ_POSTS = 'read:posts',
  WRITE_POSTS = 'write:posts',
  DELETE_POSTS = 'delete:posts',
}

const rolePermissions: Record<Role, Permission[]> = {
  [Role.ADMIN]: Object.values(Permission),
  [Role.MODERATOR]: [
    Permission.READ_USERS,
    Permission.READ_POSTS,
    Permission.WRITE_POSTS,
  ],
  [Role.USER]: [Permission.READ_USERS, Permission.READ_POSTS, Permission.WRITE_POSTS],
  [Role.GUEST]: [Permission.READ_POSTS],
};

export class AuthorizationService {
  hasPermission(userRole: Role, permission: Permission): boolean {
    return rolePermissions[userRole]?.includes(permission) || false;
  }
}
I use a strength checker (e.g. zxcvbn) and a short allowlist of rules so users get one clear message. For auth endpoints I rate limit strictly (e.g. 5 attempts per 15 minutes per IP) and skip counting successful logins so normal use doesn’t hit the limit.
Typescript
import zxcvbn from 'zxcvbn';

export class PasswordPolicy {
  private static readonly MIN_STRENGTH = 3;
  private static readonly MIN_LENGTH = 8;

  static validatePassword(password: string): { valid: boolean; errors: string[] } {
    const errors: string[] = [];
    const strength = zxcvbn(password);

    if (password.length < this.MIN_LENGTH) {
      errors.push(`Password must be at least ${this.MIN_LENGTH} characters long`);
    }

    if (strength.score < this.MIN_STRENGTH) {
      errors.push('Password is too weak. Please use a stronger password.');
    }

    if (!/[A-Z]/.test(password)) {
      errors.push('Password must contain at least one uppercase letter');
    }

    if (!/[a-z]/.test(password)) {
      errors.push('Password must contain at least one lowercase letter');
    }

    if (!/\d/.test(password)) {
      errors.push('Password must contain at least one number');
    }

    return {
      valid: errors.length === 0,
      errors,
    };
  }
}
Typescript
import rateLimit from 'express-rate-limit';

export const generalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  message: 'Too many requests from this IP, please try again later.',
  standardHeaders: true,
  legacyHeaders: false,
});

export const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  message: 'Too many authentication attempts, please try again later.',
  skipSuccessfulRequests: true,
});
Hash passwords with bcrypt, use JWTs or sessions depending on whether I need revocability, and enforce authorization on the server for every protected route. I add MFA and strict rate limiting on auth when the app handles sensitive data. One thing to double-check: every “admin only” action is gated by a server-side permission check, not just the UI. OWASP Authentication Cheat Sheet is a solid reference. Hope that helps. I'm currently looking for new challenges in the AI and Full Stack space. If you're building something interesting, let's chat.
Share this post: