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/06 11:02] pradnyaauthentication_role-based_authorization_architecture [2026/01/09 08:30] (current) – [Install Swagger packages] pradnya
Line 100: Line 100:
  
 ====== Refresh-token implementation ====== ====== Refresh-token implementation ======
 +<code>
  
-<Code>+src/ 
 +┣ domain/ 
 +┃ ┣ entities/ 
 +┃ ┃ ┗ **user.ts** 
 +┃ ┗ services/ 
 +┃ ┗ auth-service.ts 
 +┣ infrastructure/ 
 +┃ ┣ redis/ 
 +┃ ┃ ┗ **refresh-token-repo.ts** 
 +┃** ┣ security/ 
 +┃ ┃ ┣ jwt-provider.ts 
 +┃ ┃ ┗ token-utils.ts**
  
-src/ \\ ┣ domain/ \\ ┃ ┣ entities/ \\ ┃ ┃ ┗** user.ts** \\ ┃ ┗ services/ \\ ┃    ┗ auth-service.ts \\ ┣ infrastructure/ \\ ┃ ┣ redis/ \\ ┃ ┃ ┗ **refresh-token-repo.ts** \\ **┃ ┣ security/ \\ ┃ ┃ ┣ jwt-provider.ts \\ ┃ ┃ ┗ token-utils.ts** \\  \\ </Code>+</code>
  
 **Redis Refresh Token Store** **Redis Refresh Token Store**
Line 109: Line 121:
 refresh-token-store.ts refresh-token-store.ts
  <font 9px/inherit;;inherit;;inherit>ts</font>  <font 9px/inherit;;inherit;;inherit>ts</font>
-<code> 
- 
  
 +<code>
 import { redis } from "./redis-client"; import { redis } from "./redis-client";
  
 const REFRESH_PREFIX = "refresh:user:"; const REFRESH_PREFIX = "refresh:user:";
  
-export async function storeRefreshToken(userId: string, tokenId: string, ttlSeconds: number) { \\ +export async function storeRefreshToken(userId: string, tokenId: string, ttlSeconds: number) { 
-  const key = `${REFRESH_PREFIX}${userId}`; \\ +  const key = `${REFRESH_PREFIX}${userId}`; 
-  await redis.set(key, tokenId, "EX", ttlSeconds); \\+  await redis.set(key, tokenId, "EX", ttlSeconds);
 } }
  
-export async function getRefreshToken(userId: string) { \\ +export async function getRefreshToken(userId: string) { 
-  return redis.get(`${REFRESH_PREFIX}${userId}`); \\+  return redis.get(`${REFRESH_PREFIX}${userId}`);
 } }
  
-export async function deleteRefreshToken(userId: string) { \\ +export async function deleteRefreshToken(userId: string) { 
-  return redis.del(`${REFRESH_PREFIX}${userId}`); \\ +  return redis.del(`${REFRESH_PREFIX}${userId}`); 
-\\+}
  
 </code> </code>
Line 135: Line 146:
 ==== JWT Provider ==== ==== JWT Provider ====
  
-**jwt-provider.ts** +**jwt-provider.ts** <font 9px/inherit;;inherit;;inherit>ts</font>
- <font 9px/inherit;;inherit;;inherit>ts</font>+
  
 <code> <code>
- 
 import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
  
-const ACCESS_EXP = "15m"; +const ACCESS_EXP = "15m";
 const REFRESH_EXP = "7d"; const REFRESH_EXP = "7d";
  
-export function signAccessToken(user: any) {  +export function signAccessToken(user: any) { 
-  return jwt.sign(  +  return jwt.sign( 
-    {  +    { 
-      sub: user.id,  +      sub: user.id, 
-      roles: user.roles,  +      roles: user.roles, 
-      email: user.email  +      email: user.email 
-    },  +    }, 
-    process.env.JWT_ACCESS_SECRET!,  +    process.env.JWT_ACCESS_SECRET!, 
-    { expiresIn: ACCESS_EXP }  +    { expiresIn: ACCESS_EXP } 
-  ); +  );
 } }
  
-export function signRefreshToken(user: any, tokenId: string) {  +export function signRefreshToken(user: any, tokenId: string) { 
-  return jwt.sign(  +  return jwt.sign( 
-    {  +    { 
-      sub: user.id,  +      sub: user.id, 
-      tid: tokenId  +      tid: tokenId 
-    },  +    }, 
-    process.env.JWT_REFRESH_SECRET!,  +    process.env.JWT_REFRESH_SECRET!, 
-    { expiresIn: REFRESH_EXP }  +    { expiresIn: REFRESH_EXP } 
-  ); +  );
 } }
  
-export function verifyAccessToken(token: string) {  +export function verifyAccessToken(token: string) { 
-  return jwt.verify(token, process.env.JWT_ACCESS_SECRET!); +  return jwt.verify(token, process.env.JWT_ACCESS_SECRET!);
 } }
  
-export function verifyRefreshToken(token: string) {  +export function verifyRefreshToken(token: string) { 
-  return jwt.verify(token, process.env.JWT_REFRESH_SECRET!);  +  return jwt.verify(token, process.env.JWT_REFRESH_SECRET!); 
-+}
  
 JWT Provider JWT Provider
- 
  
 </code> </code>
Line 186: Line 194:
  <font 9px/inherit;;inherit;;inherit>ts</font>  <font 9px/inherit;;inherit;;inherit>ts</font>
  
-<code>import crypto from "crypto";  +<code> 
-export const generateTokenId = () => crypto.randomUUID(); +import crypto from "crypto"; 
 +export const generateTokenId = () => crypto.randomUUID();
  
 </code> </code>
  
-==== Auth Service —   Rotating Refresh Token Strategy   ====+==== Auth Service — Rotating Refresh Token Strategy ====
  
 auth-service.ts auth-service.ts
Line 198: Line 207:
  
 <code> <code>
- +import { 
-import {  +  storeRefreshToken, 
-  storeRefreshToken,  +  getRefreshToken, 
-  getRefreshToken,  +  deleteRefreshToken
-  deleteRefreshToken +
 } from "../../infrastructure/redis/refresh-token-repo"; } from "../../infrastructure/redis/refresh-token-repo";
  
-import {  +import { 
-  signAccessToken,  +  signAccessToken, 
-  signRefreshToken,  +  signRefreshToken, 
-  verifyRefreshToken +  verifyRefreshToken
 } from "../../infrastructure/security/jwt-provider"; } from "../../infrastructure/security/jwt-provider";
  
Line 215: Line 223:
 const REFRESH_TTL = 60 * 60 * 24 * 7; // 7 days const REFRESH_TTL = 60 * 60 * 24 * 7; // 7 days
  
-export async function login(user: any) { +export async function login(user: any) {
   const tokenId = generateTokenId();   const tokenId = generateTokenId();
  
   await storeRefreshToken(user.id, tokenId, REFRESH_TTL);   await storeRefreshToken(user.id, tokenId, REFRESH_TTL);
  
-  return {  +  return { 
-    accessToken: signAccessToken(user),  +    accessToken: signAccessToken(user), 
-    refreshToken: signRefreshToken(user, tokenId)  +    refreshToken: signRefreshToken(user, tokenId) 
-  }; +  };
 } }
  
-export async function refresh(refreshToken: string) { +export async function refresh(refreshToken: string) {
   const decoded: any = verifyRefreshToken(refreshToken);   const decoded: any = verifyRefreshToken(refreshToken);
  
   const storedTokenId = await getRefreshToken(decoded.sub);   const storedTokenId = await getRefreshToken(decoded.sub);
  
-  if (!storedTokenId || storedTokenId !== decoded.tid) +  if (!storedTokenId || storedTokenId !== decoded.tid)
     throw new Error("Refresh token invalid or rotated");     throw new Error("Refresh token invalid or rotated");
  
-  // ROTATE TOKEN +  // ROTATE TOKEN
   await deleteRefreshToken(decoded.sub);   await deleteRefreshToken(decoded.sub);
  
-  const newTokenId = generateTokenId(); +  const newTokenId = generateTokenId();
   await storeRefreshToken(decoded.sub, newTokenId, REFRESH_TTL);   await storeRefreshToken(decoded.sub, newTokenId, REFRESH_TTL);
  
-  return {  +  return { 
-    accessToken: signAccessToken({ id: decoded.sub }),  +    accessToken: signAccessToken({ id: decoded.sub }), 
-    refreshToken: signRefreshToken({ id: decoded.sub }, newTokenId)  +    refreshToken: signRefreshToken({ id: decoded.sub }, newTokenId) 
-  }; +  };
 } }
  
-export async function logout(userId: string) {  +export async function logout(userId: string) { 
-  await deleteRefreshToken(userId);  +  await deleteRefreshToken(userId); 
- +}
  
 </code> </code>
Line 259: Line 266:
 ts ts
 <code> <code>
- 
  
 import { login, refresh, logout } from "../../domain/services/auth-service"; import { login, refresh, logout } from "../../domain/services/auth-service";
  
-export async function loginHandler(req, res) { +export async function loginHandler(req, res) {
   const user = await req.services.userService.validate(req.body);   const user = await req.services.userService.validate(req.body);
  
   const tokens = await login(user);   const tokens = await login(user);
  
-  res  +  res 
-    .cookie("refreshToken", tokens.refreshToken, {  +    .cookie("refreshToken", tokens.refreshToken,
-      httpOnly: true,  +      httpOnly: true, 
-      secure: true,  +      secure: true, 
-      sameSite: "strict"  +      sameSite: "strict" 
-    })  +    }) 
-    .json({  +    .json({ 
-      accessToken: tokens.accessToken,  +      accessToken: tokens.accessToken, 
-      roles: user.roles  +      roles: user.roles 
-    }); +    });
 } }
  
-export async function refreshHandler(req, res) { +export async function refreshHandler(req, res) {
   const token = req.cookies.refreshToken;   const token = req.cookies.refreshToken;
  
   const tokens = await refresh(token);   const tokens = await refresh(token);
  
-  res.cookie("refreshToken", tokens.refreshToken, {  +  res.cookie("refreshToken", tokens.refreshToken,
-    httpOnly: true,  +    httpOnly: true, 
-    secure: true,  +    secure: true, 
-    sameSite: "strict" +    sameSite: "strict"
   });   });
  
-  res.json({ accessToken: tokens.accessToken }); +  res.json({ accessToken: tokens.accessToken });
 } }
  
-export async function logoutHandler(req, res) {  +export async function logoutHandler(req, res) { 
-  await logout(req.user.id);  +  await logout(req.user.id); 
-  res.clearCookie("refreshToken");  +  res.clearCookie("refreshToken"); 
-  res.sendStatus(204);  +  res.sendStatus(204); 
- +}
  
 </code> </code>
Line 307: Line 312:
  
 <code> <code>
- 
 import { verifyAccessToken } from "../../infrastructure/security/jwt-provider"; import { verifyAccessToken } from "../../infrastructure/security/jwt-provider";
  
-export function authMiddleware(req, res, next) {  +export function authMiddleware(req, res, next) { 
-  try {  +  try { 
-    const token = req.headers.authorization?.split(" ")[1];  +    const token = req.headers.authorization?.split(" ")[1]; 
-    const decoded: any = verifyAccessToken(token);  +    const decoded: any = verifyAccessToken(token); 
-    req.user = decoded;  +    req.user = decoded; 
-    next();  +    next(); 
-  } catch {  +  } catch { 
-    res.status(401).json({ message: "Unauthorized" });  +    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> </code>
Line 330: Line 350:
  
 <code> <code>
- 
 import pino from "pino"; import pino from "pino";
  
-export const logger = pino({  +export const logger = pino({ 
-  level: "info",  +  level: "info", 
-  transport: { target: "pino-pretty" }  +  transport: { target: "pino-pretty"
-});  +});
  
 </code> </code>
Line 344: Line 362:
  <font 9px/inherit;;inherit;;inherit>ts</font>  <font 9px/inherit;;inherit;;inherit>ts</font>
  
-<code>logger.info({ userId: user.id }, "refresh token rotated"); +<code> 
 +logger.info({ userId: user.id }, "refresh token rotated");
  
 </code> </code>
Line 350: Line 369:
 ===== Client — Reading Cookie ===== ===== Client — Reading Cookie =====
  
-Browser automatically sends HttpOnly cookie — **frontend CANNOT read it (secure)** +Browser automatically sends HttpOnly cookie — **frontend CANNOT read it (secure)** <font 9px/inherit;;inherit;;inherit>ts</font>
- <font 9px/inherit;;inherit;;inherit>ts</font>+
  
-<code>await fetch("/auth/refresh", { method: "POST", credentials: "include" }); +<code> 
 +await fetch("/auth/refresh", { method: "POST", credentials: "include" });
  
 </code> </code>
Line 531: Line 550:
  
 </code> </code>
 +====== Enviornment Setup  ======
  
 +**VS Code + Node + TypeScript setup for microservices**
  
 +==== Initialize Node + TypeScript ====
 +
 +<code>
 +npm init -y
 +
 +</code>
 +
 +Install core dependencies:
 +
 +<code>
 +npm install express jsonwebtoken bcryptjs pino pino-pretty dotenv mysql2
 +
 +</code>
 +
 +Install dev dependencies:
 +
 +<code>
 +npm install -D typescript ts-node-dev @types/node @types/express @types/bcryptjs @types/jsonwebtoken
 +
 +</code>
 +
 +Install dependencies for Pino logger to setup directory / folder structure
 +
 +<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>
 +npm install swagger-jsdoc swagger-ui-express
 +npm install -D @types/swagger-ui-express
 +
 +</code>
 +
 +<code>
 +npm install dotenv
 +
 +npm install bcryptjs
 +
 +npm install --save-dev @types/bcryptjs
 +
 +</code>
 +
 +install json web token
 +
 +<code>
 +npm install jsonwebtoken
 +npm install --save-dev @types/jsonwebtoken
 +
 +</code>
  
  
authentication_role-based_authorization_architecture.1767697325.txt.gz · Last modified: by pradnya