====== 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