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/06 11:04] – [JWT Strategy] pradnya | authentication_role-based_authorization_architecture [2026/01/09 08:30] (current) – [Install Swagger packages] pradnya | ||
|---|---|---|---|
| Line 97: | Line 97: | ||
| |Refresh Token|Session|7–30 days| | |Refresh Token|Session|7–30 days| | ||
| - | Roles are **NOT** stored inside JWT. | + | Roles are **NOT** |
| ====== Refresh-token implementation ====== | ====== 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** | ||
| - | src/ \\ ┣ domain/ \\ ┃ ┣ entities/ \\ ┃ ┃ ┗** user.ts** \\ ┃ ┗ services/ \\ ┃ ┗ auth-service.ts \\ ┣ infrastructure/ | + | </code> |
| **Redis Refresh Token Store** | **Redis Refresh Token Store** | ||
| Line 110: | Line 121: | ||
| refresh-token-store.ts | refresh-token-store.ts | ||
| < | < | ||
| - | < | ||
| - | |||
| + | < | ||
| import { redis } from " | import { redis } from " | ||
| const REFRESH_PREFIX = " | const REFRESH_PREFIX = " | ||
| - | export async function storeRefreshToken(userId: | + | export async function storeRefreshToken(userId: |
| - | const key = `${REFRESH_PREFIX}${userId}`; | + | const key = `${REFRESH_PREFIX}${userId}`; |
| - | await redis.set(key, | + | await redis.set(key, |
| } | } | ||
| - | export async function getRefreshToken(userId: | + | export async function getRefreshToken(userId: |
| - | return redis.get(`${REFRESH_PREFIX}${userId}`); | + | return redis.get(`${REFRESH_PREFIX}${userId}`); |
| } | } | ||
| - | export async function deleteRefreshToken(userId: | + | export async function deleteRefreshToken(userId: |
| - | return redis.del(`${REFRESH_PREFIX}${userId}`); | + | return redis.del(`${REFRESH_PREFIX}${userId}`); |
| - | } \\ | + | } |
| </ | </ | ||
| Line 136: | Line 146: | ||
| ==== JWT Provider ==== | ==== JWT Provider ==== | ||
| - | **jwt-provider.ts** | + | **jwt-provider.ts** <font 9px/ |
| - | <font 9px/ | + | |
| < | < | ||
| - | |||
| import jwt from " | import jwt from " | ||
| - | const ACCESS_EXP = " | + | const ACCESS_EXP = " |
| const REFRESH_EXP = " | const REFRESH_EXP = " | ||
| - | export function signAccessToken(user: | + | export function signAccessToken(user: |
| - | 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: | + | export function signRefreshToken(user: |
| - | 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: | + | export function verifyAccessToken(token: |
| - | return jwt.verify(token, | + | return jwt.verify(token, |
| } | } | ||
| - | export function verifyRefreshToken(token: | + | export function verifyRefreshToken(token: |
| - | return jwt.verify(token, | + | return jwt.verify(token, |
| - | } | + | } |
| JWT Provider | JWT Provider | ||
| - | |||
| </ | </ | ||
| Line 187: | Line 194: | ||
| < | < | ||
| - | < | + | < |
| - | export const generateTokenId = () => crypto.randomUUID(); | + | import crypto from " |
| + | export const generateTokenId = () => crypto.randomUUID(); | ||
| </ | </ | ||
| - | ==== Auth Service — | + | ==== Auth Service — Rotating Refresh Token Strategy ==== |
| auth-service.ts | auth-service.ts | ||
| Line 199: | Line 207: | ||
| < | < | ||
| - | + | import { | |
| - | import { | + | storeRefreshToken, |
| - | storeRefreshToken, | + | getRefreshToken, |
| - | getRefreshToken, | + | deleteRefreshToken |
| - | deleteRefreshToken | + | |
| } from " | } from " | ||
| - | import { | + | import { |
| - | signAccessToken, | + | signAccessToken, |
| - | signRefreshToken, | + | signRefreshToken, |
| - | verifyRefreshToken | + | verifyRefreshToken |
| } from " | } from " | ||
| Line 216: | 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, | await storeRefreshToken(user.id, | ||
| - | return { | + | return { |
| - | accessToken: | + | accessToken: |
| - | refreshToken: | + | refreshToken: |
| - | }; | + | }; |
| } | } | ||
| - | export async function refresh(refreshToken: | + | export async function refresh(refreshToken: |
| 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(" | throw new Error(" | ||
| - | // ROTATE TOKEN | + | // ROTATE TOKEN |
| await deleteRefreshToken(decoded.sub); | await deleteRefreshToken(decoded.sub); | ||
| - | const newTokenId = generateTokenId(); | + | const newTokenId = generateTokenId(); |
| await storeRefreshToken(decoded.sub, | await storeRefreshToken(decoded.sub, | ||
| - | return { | + | return { |
| - | accessToken: | + | accessToken: |
| - | refreshToken: | + | refreshToken: |
| - | }; | + | }; |
| } | } | ||
| - | export async function logout(userId: | + | export async function logout(userId: |
| - | await deleteRefreshToken(userId); | + | await deleteRefreshToken(userId); |
| - | } | + | } |
| </ | </ | ||
| Line 260: | Line 266: | ||
| ts | ts | ||
| < | < | ||
| - | |||
| import { login, refresh, logout } from " | import { login, refresh, logout } from " | ||
| - | export async function loginHandler(req, | + | export async function loginHandler(req, |
| 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(" | + | .cookie(" |
| - | httpOnly: true, | + | httpOnly: true, |
| - | secure: true, | + | secure: true, |
| - | sameSite: " | + | sameSite: " |
| - | }) | + | }) |
| - | .json({ | + | .json({ |
| - | accessToken: | + | accessToken: |
| - | roles: user.roles | + | roles: user.roles |
| - | }); | + | }); |
| } | } | ||
| - | export async function refreshHandler(req, | + | export async function refreshHandler(req, |
| const token = req.cookies.refreshToken; | const token = req.cookies.refreshToken; | ||
| const tokens = await refresh(token); | const tokens = await refresh(token); | ||
| - | res.cookie(" | + | res.cookie(" |
| - | httpOnly: true, | + | httpOnly: true, |
| - | secure: true, | + | secure: true, |
| - | sameSite: " | + | sameSite: " |
| }); | }); | ||
| - | res.json({ accessToken: | + | res.json({ accessToken: |
| } | } | ||
| - | export async function logoutHandler(req, | + | export async function logoutHandler(req, |
| - | await logout(req.user.id); | + | await logout(req.user.id); |
| - | res.clearCookie(" | + | res.clearCookie(" |
| - | res.sendStatus(204); | + | res.sendStatus(204); |
| - | } | + | } |
| </ | </ | ||
| Line 308: | Line 312: | ||
| < | < | ||
| - | |||
| import { verifyAccessToken } from " | import { verifyAccessToken } from " | ||
| - | export function authMiddleware(req, | + | export function authMiddleware(req, |
| - | try { | + | try { |
| - | const token = req.headers.authorization? | + | const token = req.headers.authorization? |
| - | 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: " | + | 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); | ||
| + | }); | ||
| + | }); | ||
| </ | </ | ||
| Line 331: | Line 350: | ||
| < | < | ||
| - | |||
| import pino from " | import pino from " | ||
| - | export const logger = pino({ | + | export const logger = pino({ |
| - | level: " | + | level: " |
| - | transport: { target: " | + | transport: { target: " |
| - | }); | + | }); |
| </ | </ | ||
| Line 345: | Line 362: | ||
| < | < | ||
| - | < | + | < |
| + | logger.info({ userId: user.id }, " | ||
| </ | </ | ||
| Line 351: | 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/ |
| - | <font 9px/ | + | |
| - | < | + | < |
| + | await fetch("/ | ||
| </ | </ | ||
| Line 532: | Line 550: | ||
| </ | </ | ||
| + | ====== 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/ | ||
| + | |||
| + | </ | ||
| + | |||
| + | Install dependencies for Pino logger to setup directory / folder structure | ||
| + | |||
| + | < | ||
| + | </ | ||
| + | |||
| + | |||
| + | ==== 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/ | ||
| + | |||
| + | </ | ||
| + | |||
| + | < | ||
| + | npm install dotenv | ||
| + | |||
| + | 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.1767697457.txt.gz · Last modified: by pradnya
