Table of Contents
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
