authentication_role-based_authorization_architecture
Differences
This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
| authentication_role-based_authorization_architecture [2026/01/05 07:34] – [Auth Middleware] pradnya | authentication_role-based_authorization_architecture [2026/01/09 08:30] (current) – [Install Swagger packages] pradnya | ||
|---|---|---|---|
| Line 98: | Line 98: | ||
| Roles are **NOT** | Roles are **NOT** | ||
| + | |||
| + | ====== 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 | ||
| + | < | ||
| + | |||
| + | < | ||
| + | import { redis } from " | ||
| + | |||
| + | const REFRESH_PREFIX = " | ||
| + | |||
| + | export async function storeRefreshToken(userId: | ||
| + | const key = `${REFRESH_PREFIX}${userId}`; | ||
| + | await redis.set(key, | ||
| + | } | ||
| + | |||
| + | export async function getRefreshToken(userId: | ||
| + | return redis.get(`${REFRESH_PREFIX}${userId}`); | ||
| + | } | ||
| + | |||
| + | export async function deleteRefreshToken(userId: | ||
| + | return redis.del(`${REFRESH_PREFIX}${userId}`); | ||
| + | } | ||
| + | |||
| + | </ | ||
| + | |||
| + | TTL determines refresh lifetime (e.g., **7 days**) | ||
| + | |||
| + | ==== JWT Provider ==== | ||
| + | |||
| + | **jwt-provider.ts** <font 9px/ | ||
| + | |||
| + | < | ||
| + | import jwt from " | ||
| + | |||
| + | const ACCESS_EXP = " | ||
| + | const REFRESH_EXP = " | ||
| + | |||
| + | export function signAccessToken(user: | ||
| + | return jwt.sign( | ||
| + | { | ||
| + | sub: user.id, | ||
| + | roles: user.roles, | ||
| + | email: user.email | ||
| + | }, | ||
| + | process.env.JWT_ACCESS_SECRET!, | ||
| + | { expiresIn: ACCESS_EXP } | ||
| + | ); | ||
| + | } | ||
| + | |||
| + | export function signRefreshToken(user: | ||
| + | return jwt.sign( | ||
| + | { | ||
| + | sub: user.id, | ||
| + | tid: tokenId | ||
| + | }, | ||
| + | process.env.JWT_REFRESH_SECRET!, | ||
| + | { expiresIn: REFRESH_EXP } | ||
| + | ); | ||
| + | } | ||
| + | |||
| + | export function verifyAccessToken(token: | ||
| + | return jwt.verify(token, | ||
| + | } | ||
| + | |||
| + | export function verifyRefreshToken(token: | ||
| + | return jwt.verify(token, | ||
| + | } | ||
| + | |||
| + | JWT Provider | ||
| + | |||
| + | </ | ||
| + | |||
| + | ==== Token Utility (ID generator) ==== | ||
| + | |||
| + | token-utils.ts | ||
| + | < | ||
| + | |||
| + | < | ||
| + | import crypto from " | ||
| + | export const generateTokenId = () => crypto.randomUUID(); | ||
| + | |||
| + | </ | ||
| + | |||
| + | ==== Auth Service — Rotating Refresh Token Strategy ==== | ||
| + | |||
| + | auth-service.ts | ||
| + | |||
| + | ts | ||
| + | |||
| + | < | ||
| + | import { | ||
| + | storeRefreshToken, | ||
| + | getRefreshToken, | ||
| + | deleteRefreshToken | ||
| + | } from " | ||
| + | |||
| + | import { | ||
| + | signAccessToken, | ||
| + | signRefreshToken, | ||
| + | verifyRefreshToken | ||
| + | } from " | ||
| + | |||
| + | import { generateTokenId } from " | ||
| + | |||
| + | const REFRESH_TTL = 60 * 60 * 24 * 7; // 7 days | ||
| + | |||
| + | export async function login(user: any) { | ||
| + | const tokenId = generateTokenId(); | ||
| + | |||
| + | await storeRefreshToken(user.id, | ||
| + | |||
| + | return { | ||
| + | accessToken: | ||
| + | refreshToken: | ||
| + | }; | ||
| + | } | ||
| + | |||
| + | export async function refresh(refreshToken: | ||
| + | const decoded: any = verifyRefreshToken(refreshToken); | ||
| + | |||
| + | const storedTokenId = await getRefreshToken(decoded.sub); | ||
| + | |||
| + | if (!storedTokenId || storedTokenId !== decoded.tid) | ||
| + | throw new Error(" | ||
| + | |||
| + | // ROTATE TOKEN | ||
| + | await deleteRefreshToken(decoded.sub); | ||
| + | |||
| + | const newTokenId = generateTokenId(); | ||
| + | await storeRefreshToken(decoded.sub, | ||
| + | |||
| + | return { | ||
| + | accessToken: | ||
| + | refreshToken: | ||
| + | }; | ||
| + | } | ||
| + | |||
| + | export async function logout(userId: | ||
| + | await deleteRefreshToken(userId); | ||
| + | } | ||
| + | |||
| + | </ | ||
| + | |||
| + | ==== Auth Controller ==== | ||
| + | |||
| + | auth-controller.ts | ||
| + | |||
| + | ts | ||
| + | < | ||
| + | |||
| + | import { login, refresh, logout } from " | ||
| + | |||
| + | export async function loginHandler(req, | ||
| + | const user = await req.services.userService.validate(req.body); | ||
| + | |||
| + | const tokens = await login(user); | ||
| + | |||
| + | res | ||
| + | .cookie(" | ||
| + | httpOnly: true, | ||
| + | secure: true, | ||
| + | sameSite: " | ||
| + | }) | ||
| + | .json({ | ||
| + | accessToken: | ||
| + | roles: user.roles | ||
| + | }); | ||
| + | } | ||
| + | |||
| + | export async function refreshHandler(req, | ||
| + | const token = req.cookies.refreshToken; | ||
| + | |||
| + | const tokens = await refresh(token); | ||
| + | |||
| + | res.cookie(" | ||
| + | httpOnly: true, | ||
| + | secure: true, | ||
| + | sameSite: " | ||
| + | }); | ||
| + | |||
| + | res.json({ accessToken: | ||
| + | } | ||
| + | |||
| + | export async function logoutHandler(req, | ||
| + | await logout(req.user.id); | ||
| + | res.clearCookie(" | ||
| + | res.sendStatus(204); | ||
| + | } | ||
| + | |||
| + | </ | ||
| + | |||
| + | ==== Auth Middleware ==== | ||
| + | < | ||
| + | |||
| + | < | ||
| + | import { verifyAccessToken } from " | ||
| + | |||
| + | export function authMiddleware(req, | ||
| + | try { | ||
| + | const token = req.headers.authorization? | ||
| + | const decoded: any = verifyAccessToken(token); | ||
| + | req.user = decoded; | ||
| + | next(); | ||
| + | } catch { | ||
| + | res.status(401).json({ message: " | ||
| + | } | ||
| + | } | ||
| + | |||
| + | </ | ||
| + | |||
| + | ==== Jest Test ==== | ||
| + | < | ||
| + | |||
| + | < | ||
| + | describe(" | ||
| + | it(" | ||
| + | const loginRes = await login({ id: " | ||
| + | |||
| + | const refreshRes = await refresh(loginRes.refreshToken); | ||
| + | |||
| + | expect(refreshRes.accessToken).toBeDefined(); | ||
| + | expect(refreshRes.refreshToken).not.toBe(loginRes.refreshToken); | ||
| + | }); | ||
| + | }); | ||
| + | |||
| + | </ | ||
| + | |||
| + | ===== Logging + Correlation ===== | ||
| + | |||
| + | logger.ts | ||
| + | < | ||
| + | |||
| + | < | ||
| + | import pino from " | ||
| + | |||
| + | export const logger = pino({ | ||
| + | level: " | ||
| + | transport: { target: " | ||
| + | }); | ||
| + | |||
| + | </ | ||
| + | |||
| + | Add logs in auth-service: | ||
| + | < | ||
| + | |||
| + | < | ||
| + | logger.info({ userId: user.id }, " | ||
| + | |||
| + | </ | ||
| + | |||
| + | ===== Client — Reading Cookie ===== | ||
| + | |||
| + | Browser automatically sends HttpOnly cookie — **frontend CANNOT read it (secure)** <font 9px/ | ||
| + | |||
| + | < | ||
| + | await fetch("/ | ||
| + | |||
| + | </ | ||
| ==== Role Retrieval Strategy (Best Practice) ==== | ==== Role Retrieval Strategy (Best Practice) ==== | ||
| Line 250: | Line 527: | ||
| </ | </ | ||
| + | ===== Jest Tests (Example) ===== | ||
| + | < | ||
| - | ===== Role Enforcement Middleware ===== | + | < |
| + | describe(" | ||
| + | it(" | ||
| + | await redis.set(" | ||
| + | expect(await getUserRoles(" | ||
| + | }); | ||
| + | }); | ||
| - | authorize.ts | + | </ |
| - | < | + | |
| + | ===== Swagger Example ===== | ||
| + | < | ||
| < | < | ||
| - | export function requireRole(role: string) { | + | securitySchemes: |
| - | return (req, res, next) => { | + | bearerAuth: |
| - | | + | |
| - | return res.sendStatus(403); | + | |
| - | | + | |
| - | next(); | + | |
| - | }; | + | |
| - | } | + | |
| </ | </ | ||
| + | ====== Enviornment Setup ====== | ||
| - | ===== Role Caching Layer ===== | + | **VS Code + Node + TypeScript setup for microservices** |
| - | role-service.ts | + | ==== Initialize Node + TypeScript ==== |
| - | < | + | |
| < | < | ||
| - | import { redis } from " | + | npm init -y |
| - | import { userRoleRepo } from " | + | |
| - | export async function getUserRoles(userId: | + | </ |
| - | const cacheKey = `roles: | + | |
| - | const cached = await redis.get(cacheKey); | + | Install core dependencies: |
| - | if (cached) return JSON.parse(cached); | + | |
| - | const roles = await userRoleRepo.getRoles(userId); | + | < |
| + | npm install express jsonwebtoken bcryptjs pino pino-pretty dotenv mysql2 | ||
| - | await redis.set(cacheKey, | + | </ |
| - | return roles; | + | Install dev dependencies: |
| - | } | + | |
| + | < | ||
| + | npm install -D typescript ts-node-dev @types/node @types/ | ||
| </ | </ | ||
| - | ===== Jest Tests (Example) ===== | + | Install dependencies for Pino logger to setup directory / folder structure |
| - | <font 9px/ | + | |
| + | < | ||
| + | </ | ||
| + | |||
| + | |||
| + | ==== Install Swagger packages | ||
| + | |||
| + | Install swagger packages on the root directory or directory with tsconfig | ||
| < | < | ||
| - | describe(" | + | npm install swagger-jsdoc swagger-ui-express |
| - | it(" | + | npm install -D @types/ |
| - | await redis.set(" | + | |
| - | expect(await getUserRoles(" | + | |
| - | }); | + | |
| - | }); | + | |
| </ | </ | ||
| - | |||
| - | ===== Swagger Example ===== | ||
| - | < | ||
| < | < | ||
| - | securitySchemes: | + | npm install dotenv |
| - | bearerAuth: | + | |
| - | type: http | + | npm install bcryptjs |
| - | | + | |
| + | npm install --save-dev @types/ | ||
| </ | </ | ||
| + | install json web token | ||
| + | < | ||
| + | npm install jsonwebtoken | ||
| + | npm install --save-dev @types/ | ||
| + | |||
| + | </ | ||
authentication_role-based_authorization_architecture.1767598477.txt.gz · Last modified: by pradnya
