User Tools

Site Tools


authentication_role-based_authorization_architecture

This is an old revision of the document!


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

TokenPurposeExpiry
Access TokenIdentity15 mins
Refresh TokenSession7–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" }); 
  } 
} 

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"); 

Browser automatically sends HttpOnly cookie — frontend CANNOT read it (secure) ts

await fetch("/auth/refresh", { method: "POST", credentials: "include" }); 

Role Retrieval Strategy (Best Practice)

SourcePurpose
MySQLSource of truth
RedisPerformance 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
authentication_role-based_authorization_architecture.1767697493.txt.gz · Last modified: by pradnya