====== Authentication + Role-Based Authorization Architecture ====== ===== Overview ===== This BFF handles: * Authentication * Role & permission resolution (from MySQL) * Token lifecycle * Secure API gateway behavior * UI-friendly role exposure * Backend authorization enforcement Frontend uses: * Roles → feature toggling * Backend → strict enforcement ===== Clean Architecture Directory Structure ===== src/ ┣ api/ ┃ ┣ controllers/ ┃ ┃ ┣ auth-controller.ts ┃ ┃ ┗ user-controller.ts ┃ ┣ middleware/ ┃ ┃ ┣ auth-middleware.ts ┃ ┃ ┣ authorize.ts ┃ ┃ ┗ error-middleware.ts ┃ ┗ routes/ ┃ ┣ auth-routes.ts ┃ ┗ user-routes.ts ┣ domain/ ┃ ┣ auth/ ┃ ┃ ┣ auth-service.ts ┃ ┃ ┗ role-service.ts ┃ ┣ user/ ┃ ┃ ┗ user-service.ts ┗ infrastructure/ ┣ db/ ┃ ┣ mysql-connection.ts ┃ ┗ user-role-repo.ts ┣ cache/ ┃ ┗ redis-client.ts ┣ auth/ ┃ ┣ jwt-provider.ts ┃ ┗ token-store.ts ┣ logging/ ┃ ┗ logger.ts ┗ config/ ┗ env.ts ===== Authentication & Authorization Flow ===== **Flow Summary** Login → verify user → load roles → cache → JWT issued → user DTO returned Every request → verify JWT → get roles (cache first) → enforce policy ===== API Endpoints ===== ==== POST /auth/login ==== { "accessToken": "xxx", "refreshToken": "yyy", "expiresIn": 900, "user": { "id": "u123", "displayName": "John", "roles": ["BOOKING_CREATE"], "permissions": ["booking.create"] } } ==== POST /auth/refresh ==== Returns fresh token + roles. ==== GET /auth/me ==== Returns latest user profile. ==== JWT Strategy ==== ^Token^Purpose^Expiry| |Access Token|Identity|15 mins| |Refresh Token|Session|7–30 days| Roles are **NOT** stored inside JWT. ====== Refresh-token implementation ====== src/ ┣ domain/ ┃ ┣ entities/ ┃ ┃ ┗ **user.ts** ┃ ┗ services/ ┃ ┗ auth-service.ts ┣ infrastructure/ ┃ ┣ redis/ ┃ ┃ ┗ **refresh-token-repo.ts** ┃** ┣ security/ ┃ ┃ ┣ jwt-provider.ts ┃ ┃ ┗ token-utils.ts** **Redis Refresh Token Store** refresh-token-store.ts ts import { redis } from "./redis-client"; const REFRESH_PREFIX = "refresh:user:"; export async function storeRefreshToken(userId: string, tokenId: string, ttlSeconds: number) { const key = `${REFRESH_PREFIX}${userId}`; await redis.set(key, tokenId, "EX", ttlSeconds); } export async function getRefreshToken(userId: string) { return redis.get(`${REFRESH_PREFIX}${userId}`); } export async function deleteRefreshToken(userId: string) { return redis.del(`${REFRESH_PREFIX}${userId}`); } TTL determines refresh lifetime (e.g., **7 days**) ==== JWT Provider ==== **jwt-provider.ts** ts import jwt from "jsonwebtoken"; const ACCESS_EXP = "15m"; const REFRESH_EXP = "7d"; export function signAccessToken(user: any) { return jwt.sign( { sub: user.id, roles: user.roles, email: user.email }, process.env.JWT_ACCESS_SECRET!, { expiresIn: ACCESS_EXP } ); } export function signRefreshToken(user: any, tokenId: string) { return jwt.sign( { sub: user.id, tid: tokenId }, process.env.JWT_REFRESH_SECRET!, { expiresIn: REFRESH_EXP } ); } export function verifyAccessToken(token: string) { return jwt.verify(token, process.env.JWT_ACCESS_SECRET!); } export function verifyRefreshToken(token: string) { return jwt.verify(token, process.env.JWT_REFRESH_SECRET!); } JWT Provider ==== Token Utility (ID generator) ==== token-utils.ts ts import crypto from "crypto"; export const generateTokenId = () => crypto.randomUUID(); ==== Auth Service — Rotating Refresh Token Strategy ==== auth-service.ts ts import { storeRefreshToken, getRefreshToken, deleteRefreshToken } from "../../infrastructure/redis/refresh-token-repo"; import { signAccessToken, signRefreshToken, verifyRefreshToken } from "../../infrastructure/security/jwt-provider"; import { generateTokenId } from "../../infrastructure/security/token-utils"; const REFRESH_TTL = 60 * 60 * 24 * 7; // 7 days export async function login(user: any) { const tokenId = generateTokenId(); await storeRefreshToken(user.id, tokenId, REFRESH_TTL); return { accessToken: signAccessToken(user), refreshToken: signRefreshToken(user, tokenId) }; } export async function refresh(refreshToken: string) { const decoded: any = verifyRefreshToken(refreshToken); const storedTokenId = await getRefreshToken(decoded.sub); if (!storedTokenId || storedTokenId !== decoded.tid) throw new Error("Refresh token invalid or rotated"); // ROTATE TOKEN await deleteRefreshToken(decoded.sub); const newTokenId = generateTokenId(); await storeRefreshToken(decoded.sub, newTokenId, REFRESH_TTL); return { accessToken: signAccessToken({ id: decoded.sub }), refreshToken: signRefreshToken({ id: decoded.sub }, newTokenId) }; } export async function logout(userId: string) { await deleteRefreshToken(userId); } ==== Auth Controller ==== auth-controller.ts ts import { login, refresh, logout } from "../../domain/services/auth-service"; export async function loginHandler(req, res) { const user = await req.services.userService.validate(req.body); const tokens = await login(user); res .cookie("refreshToken", tokens.refreshToken, { httpOnly: true, secure: true, sameSite: "strict" }) .json({ accessToken: tokens.accessToken, roles: user.roles }); } export async function refreshHandler(req, res) { const token = req.cookies.refreshToken; const tokens = await refresh(token); res.cookie("refreshToken", tokens.refreshToken, { httpOnly: true, secure: true, sameSite: "strict" }); res.json({ accessToken: tokens.accessToken }); } export async function logoutHandler(req, res) { await logout(req.user.id); res.clearCookie("refreshToken"); res.sendStatus(204); } ==== Auth Middleware ==== ts import { verifyAccessToken } from "../../infrastructure/security/jwt-provider"; export function authMiddleware(req, res, next) { try { const token = req.headers.authorization?.split(" ")[1]; const decoded: any = verifyAccessToken(token); req.user = decoded; next(); } catch { res.status(401).json({ message: "Unauthorized" }); } } ==== Jest Test ==== ts describe("refresh flow", () => { it("rotates refresh tokens", async () => { const loginRes = await login({ id: "1" }); const refreshRes = await refresh(loginRes.refreshToken); expect(refreshRes.accessToken).toBeDefined(); expect(refreshRes.refreshToken).not.toBe(loginRes.refreshToken); }); }); ===== Logging + Correlation ===== logger.ts ts import pino from "pino"; export const logger = pino({ level: "info", transport: { target: "pino-pretty" } }); Add logs in auth-service: ts logger.info({ userId: user.id }, "refresh token rotated"); ===== Client — Reading Cookie ===== Browser automatically sends HttpOnly cookie — **frontend CANNOT read it (secure)** ts await fetch("/auth/refresh", { method: "POST", credentials: "include" }); ==== Role Retrieval Strategy (Best Practice) ==== ^Source^Purpose| |MySQL|Source of truth| |Redis|Performance cache| Cache duration = **5–10 minutes** ===== Redis Caching Implementation ===== redis-client.ts ts import Redis from "ioredis"; export const redis = new Redis({ host: process.env.REDIS_HOST, ttl: 600 // 10 mins }); Logging with Pino + Trace IDs ts import pino from "pino"; import { v4 as uuid } from "uuid"; export const logger = pino({ level: process.env.LOG_LEVEL || "info", transport: process.env.NODE_ENV === "development" ? { target: "pino-pretty" } : undefined }); export function withTraceId(req, res, next) { req.traceId = req.headers["x-trace-id"] || uuid(); logger.info({ traceId: req.traceId }, "Request started"); next(); } ===== Swagger Setup ===== swagger.ts ts import swaggerUi from "swagger-ui-express"; import swaggerJsDoc from "swagger-jsdoc"; const spec = swaggerJsDoc({ definition: { openapi: "3.0.0", info: { title: "BFF API", version: "1.0.0" } }, apis: ["./src/api/routes/*.ts"] }); export default (app) => { app.use("/docs", swaggerUi.serve, swaggerUi.setup(spec)); }; ===== JWT Provider ===== jwt-provider.ts ts import jwt from "jsonwebtoken"; export function signAccessToken(userId: string) { return jwt.sign({ sub: userId }, process.env.JWT_SECRET!, { expiresIn: "15m" }); } export function verifyAccessToken(token: string) { return jwt.verify(token, process.env.JWT_SECRET!); } ===== Auth Middleware ===== auth-middleware.ts ts import { verifyAccessToken } from "../../infrastructure/auth/jwt-provider"; export function authMiddleware(req, res, next) { const token = req.headers.authorization?.split(" ")[1]; if (!token) return res.sendStatus(401); try { req.user = verifyAccessToken(token); next(); } catch { return res.sendStatus(401); } } ===== Role Enforcement Middleware ===== authorize.ts ts export function requireRole(role: string) { return (req, res, next) => { if (!req.user?.roles?.includes(role)) { return res.sendStatus(403); } next(); }; } ===== Role Caching Layer ===== role-service.ts ts import { redis } from "../../infrastructure/cache/redis-client"; import { userRoleRepo } from "../../infrastructure/db/user-role-repo"; export async function getUserRoles(userId: string) { const cacheKey = `roles:${userId}`; const cached = await redis.get(cacheKey); if (cached) return JSON.parse(cached); const roles = await userRoleRepo.getRoles(userId); await redis.set(cacheKey, JSON.stringify(roles), "EX", 600); return roles; } ===== Jest Tests (Example) ===== ts describe("role cache", () => { it("returns cached role", async () => { await redis.set("roles:u1", JSON.stringify(["ADMIN"])); expect(await getUserRoles("u1")).toContain("ADMIN"); }); }); ===== Swagger Example ===== yaml securitySchemes: bearerAuth: type: http scheme: bearer ====== Enviornment Setup ====== **VS Code + Node + TypeScript setup for microservices** ==== Initialize Node + TypeScript ==== npm init -y Install core dependencies: npm install express jsonwebtoken bcryptjs pino pino-pretty dotenv mysql2 Install dev dependencies: npm install -D typescript ts-node-dev @types/node @types/express @types/bcryptjs @types/jsonwebtoken Install dependencies for Pino logger to setup directory / folder structure npm install pino pino-pretty pino-multi-stream fs-extra ==== Install Swagger packages ==== Install swagger packages on the root directory or directory with tsconfig npm install swagger-jsdoc swagger-ui-express npm install -D @types/swagger-ui-express npm install dotenv npm install bcryptjs npm install --save-dev @types/bcryptjs install json web token npm install jsonwebtoken npm install --save-dev @types/jsonwebtoken