User Tools

Site Tools


authentication_role-based_authorization_architecture

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
authentication_role-based_authorization_architecture [2026/01/05 07:34] – [Auth Middleware] pradnyaauthentication_role-based_authorization_architecture [2026/01/09 08:30] (current) – [Install Swagger packages] pradnya
Line 98: Line 98:
  
 Roles are **NOT**  stored inside JWT. Roles are **NOT**  stored inside JWT.
 +
 +====== Refresh-token implementation ======
 +<code>
 +
 +src/
 +┣ domain/
 +┃ ┣ entities/
 +┃ ┃ ┗ **user.ts**
 +┃ ┗ services/
 +┃ ┗ auth-service.ts
 +┣ infrastructure/
 +┃ ┣ redis/
 +┃ ┃ ┗ **refresh-token-repo.ts**
 +┃** ┣ security/
 +┃ ┃ ┣ jwt-provider.ts
 +┃ ┃ ┗ token-utils.ts**
 +
 +</code>
 +
 +**Redis Refresh Token Store**
 +
 +refresh-token-store.ts
 + <font 9px/inherit;;inherit;;inherit>ts</font>
 +
 +<code>
 +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}`);
 +}
 +
 +</code>
 +
 +TTL determines refresh lifetime (e.g., **7 days**)
 +
 +==== JWT Provider ====
 +
 +**jwt-provider.ts** <font 9px/inherit;;inherit;;inherit>ts</font>
 +
 +<code>
 +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
 +
 +</code>
 +
 +==== Token Utility (ID generator) ====
 +
 +token-utils.ts
 + <font 9px/inherit;;inherit;;inherit>ts</font>
 +
 +<code>
 +import crypto from "crypto";
 +export const generateTokenId = () => crypto.randomUUID();
 +
 +</code>
 +
 +==== Auth Service — Rotating Refresh Token Strategy ====
 +
 +auth-service.ts
 +
 +ts
 +
 +<code>
 +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);
 +}
 +
 +</code>
 +
 +==== Auth Controller ====
 +
 +auth-controller.ts
 +
 +ts
 +<code>
 +
 +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);
 +}
 +
 +</code>
 +
 +==== Auth Middleware ====
 + <font 9px/inherit;;inherit;;inherit>ts</font>
 +
 +<code>
 +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" });
 +  }
 +}
 +
 +</code>
 +
 +==== Jest Test ====
 + <font 9px/inherit;;inherit;;inherit>ts</font>
 +
 +<code>
 +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);
 +});
 +});
 +
 +</code>
 +
 +===== Logging + Correlation =====
 +
 +logger.ts
 + <font 9px/inherit;;inherit;;inherit>ts</font>
 +
 +<code>
 +import pino from "pino";
 +
 +export const logger = pino({
 +  level: "info",
 +  transport: { target: "pino-pretty" }
 +});
 +
 +</code>
 +
 +Add logs in auth-service:
 + <font 9px/inherit;;inherit;;inherit>ts</font>
 +
 +<code>
 +logger.info({ userId: user.id }, "refresh token rotated");
 +
 +</code>
 +
 +===== Client — Reading Cookie =====
 +
 +Browser automatically sends HttpOnly cookie — **frontend CANNOT read it (secure)** <font 9px/inherit;;inherit;;inherit>ts</font>
 +
 +<code>
 +await fetch("/auth/refresh", { method: "POST", credentials: "include" });
 +
 +</code>
  
 ==== Role Retrieval Strategy (Best Practice) ==== ==== Role Retrieval Strategy (Best Practice) ====
Line 250: Line 527:
 </code> </code>
  
 +===== Jest Tests (Example) =====
 + <font 9px/inherit;;inherit;;inherit>ts</font>
  
-===== Role Enforcement Middleware =====+<code> 
 +describe("role cache", () => { 
 +it("returns cached role", async () => { 
 +  await redis.set("roles:u1", JSON.stringify(["ADMIN"])); 
 +  expect(await getUserRoles("u1")).toContain("ADMIN"); 
 +}); 
 +});
  
-authorize.ts +</code> 
- <font 9px/inherit;;inherit;;inherit>ts</font>+ 
 +===== Swagger Example ===== 
 + <font 9px/inherit;;inherit;;inherit>yaml</font>
  
 <code> <code>
-export function requireRole(rolestring) { +securitySchemes
-return (req, res, next) => { +bearerAuth: 
-  if (!req.user?.roles?.includes(role)) { +  type: http 
-    return res.sendStatus(403); +  scheme: bearer
-  +
-  next(); +
-}; +
-}+
  
 </code> </code>
 +====== Enviornment Setup  ======
  
-===== Role Caching Layer =====+**VS Code + Node + TypeScript setup for microservices**
  
-role-service.ts +==== Initialize Node + TypeScript ====
- <font 9px/inherit;;inherit;;inherit>ts</font>+
  
 <code> <code>
-import { redis } from "../../infrastructure/cache/redis-client"; +npm init -y
-import { userRoleRepo } from "../../infrastructure/db/user-role-repo";+
  
-export async function getUserRoles(userId: string) { +</code>
-const cacheKey = `roles:${userId}`;+
  
-  const cached = await redis.get(cacheKey); +Install core dependencies:
-if (cached) return JSON.parse(cached);+
  
-  const roles = await userRoleRepo.getRoles(userId);+<code> 
 +npm install express jsonwebtoken bcryptjs pino pino-pretty dotenv mysql2
  
-  await redis.set(cacheKey, JSON.stringify(roles), "EX", 600);+</code>
  
-  return roles; +Install dev dependencies: 
-}+ 
 +<code> 
 +npm install -D typescript ts-node-dev @types/node @types/express @types/bcryptjs @types/jsonwebtoken
  
 </code> </code>
  
-===== Jest Tests (Example) ===== +Install dependencies for Pino logger to setup directory / folder structure 
- <font 9px/inherit;;inherit;;inherit>ts</font>+ 
 +<code>npm install pino pino-pretty pino-multi-stream fs-extra 
 +</code> 
 + 
 + 
 +==== Install Swagger packages ==== 
 + 
 +Install swagger packages on the root directory or directory with tsconfig
  
 <code> <code>
-describe("role cache", () => { +npm install swagger-jsdoc swagger-ui-express 
-it("returns cached role", async () => { +npm install -D @types/swagger-ui-express
-  await redis.set("roles:u1", JSON.stringify(["ADMIN"])); +
-  expect(await getUserRoles("u1")).toContain("ADMIN"); +
-}); +
-});+
  
 </code> </code>
- 
-===== Swagger Example ===== 
- <font 9px/inherit;;inherit;;inherit>yaml</font> 
  
 <code> <code>
-securitySchemes: +npm install dotenv 
-bearerAuth: + 
-  type: http +npm install bcryptjs 
-  scheme: bearer+ 
 +npm install --save-dev @types/bcryptjs
  
 </code> </code>
  
 +install json web token
  
 +<code>
 +npm install jsonwebtoken
 +npm install --save-dev @types/jsonwebtoken
 +
 +</code>
  
  
authentication_role-based_authorization_architecture.1767598477.txt.gz · Last modified: by pradnya