From a7b0ae9cdc54ed6ace11ef1e3ceee0385b2ff030 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Fri, 20 Mar 2026 07:45:58 -0700 Subject: [PATCH] refactor(backend): migrate auth.ts + request-context.ts to @bytelyst/fastify-auth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - auth.ts: 80→18 lines, delegates to createAuthMiddleware() - request-context.ts: delegates to createRequestContext() - Re-exports JwtPayload, AuthPayload from shared package --- backend/package-lock.json | 22 ++++++++ backend/package.json | 1 + backend/src/lib/auth.ts | 84 ++++-------------------------- backend/src/lib/request-context.ts | 37 +++---------- 4 files changed, 41 insertions(+), 103 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 3a2eb73..c978a04 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -14,6 +14,7 @@ "@bytelyst/cosmos": "file:../../learning_ai_common_plat/packages/cosmos", "@bytelyst/datastore": "file:../../learning_ai_common_plat/packages/datastore", "@bytelyst/errors": "file:../../learning_ai_common_plat/packages/errors", + "@bytelyst/fastify-auth": "file:../../learning_ai_common_plat/packages/fastify-auth", "@bytelyst/fastify-core": "file:../../learning_ai_common_plat/packages/fastify-core", "fastify": "^5.2.1", "jose": "^6.0.8", @@ -84,6 +85,23 @@ "name": "@bytelyst/errors", "version": "0.1.0" }, + "../../learning_ai_common_plat/packages/fastify-auth": { + "name": "@bytelyst/fastify-auth", + "version": "0.1.0", + "dependencies": { + "@bytelyst/errors": "workspace:*" + }, + "devDependencies": { + "fastify": "^5.2.1", + "jose": "^6.0.8", + "typescript": "^5.7.3", + "vitest": "^3.0.5" + }, + "peerDependencies": { + "fastify": ">=5.0.0", + "jose": ">=5.0.0" + } + }, "../../learning_ai_common_plat/packages/fastify-core": { "name": "@bytelyst/fastify-core", "version": "0.1.0", @@ -357,6 +375,10 @@ "resolved": "../../learning_ai_common_plat/packages/errors", "link": true }, + "node_modules/@bytelyst/fastify-auth": { + "resolved": "../../learning_ai_common_plat/packages/fastify-auth", + "link": true + }, "node_modules/@bytelyst/fastify-core": { "resolved": "../../learning_ai_common_plat/packages/fastify-core", "link": true diff --git a/backend/package.json b/backend/package.json index 36c0e9d..50a0f68 100644 --- a/backend/package.json +++ b/backend/package.json @@ -18,6 +18,7 @@ "@bytelyst/cosmos": "file:../../learning_ai_common_plat/packages/cosmos", "@bytelyst/datastore": "file:../../learning_ai_common_plat/packages/datastore", "@bytelyst/errors": "file:../../learning_ai_common_plat/packages/errors", + "@bytelyst/fastify-auth": "file:../../learning_ai_common_plat/packages/fastify-auth", "@bytelyst/fastify-core": "file:../../learning_ai_common_plat/packages/fastify-core", "@azure/cosmos": "^4.2.0", "fastify": "^5.2.1", diff --git a/backend/src/lib/auth.ts b/backend/src/lib/auth.ts index 3771de5..516849a 100644 --- a/backend/src/lib/auth.ts +++ b/backend/src/lib/auth.ts @@ -1,79 +1,17 @@ /** - * JWT auth middleware — RS256 JWKS verification with HS256 fallback. - * Tries RS256 via platform-service JWKS endpoint first (if PLATFORM_JWKS_URL - * is configured), then falls back to HS256 symmetric verification via JWT_SECRET. + * JWT auth middleware — delegates to @bytelyst/fastify-auth. + * RS256 JWKS verification with HS256 fallback, configured from local config. + * + * Uses getter functions so config is read on each call (supports test mocks). */ -import { jwtVerify, createRemoteJWKSet } from 'jose'; -import { UnauthorizedError, ForbiddenError } from '@bytelyst/errors'; +import { createAuthMiddleware } from '@bytelyst/fastify-auth'; import { config } from './config.js'; -import type { AuthPayload } from '@bytelyst/auth'; -export type { AuthPayload }; +export type { AuthPayload } from '@bytelyst/fastify-auth'; -// Lazy-init JWKS client (cached, auto-refreshed by jose) -let jwks: ReturnType | null = null; -let jwksUrl: string | undefined; +const { extractAuth, requireRole } = createAuthMiddleware({ + jwtSecret: () => config.JWT_SECRET, + jwksUrl: () => config.PLATFORM_JWKS_URL, +}); -function getJWKS(): ReturnType | null { - const url = config.PLATFORM_JWKS_URL; - if (!url) return null; - if (jwks && jwksUrl === url) return jwks; - jwks = createRemoteJWKSet(new URL(url)); - jwksUrl = url; - return jwks; -} - -function getHmacSecret(): Uint8Array { - return new TextEncoder().encode(config.JWT_SECRET); -} - -/** - * Extract and verify auth payload from an Authorization header. - * Tries RS256 via JWKS first, falls back to HS256. - */ -export async function extractAuth(req: { - headers: { authorization?: string }; -}): Promise { - const auth = req.headers.authorization; - if (!auth?.startsWith('Bearer ')) { - throw new UnauthorizedError(); - } - const token = auth.slice(7); - - // Try RS256 via JWKS first - const remoteJWKS = getJWKS(); - if (remoteJWKS) { - try { - const { payload } = await jwtVerify(token, remoteJWKS); - const p = payload as unknown as AuthPayload; - if (p.type !== 'access') throw new Error('Not an access token'); - return p; - } catch { - // Fall through to HS256 - } - } - - // Fall back to HS256 (existing behavior) - try { - const { payload } = await jwtVerify(token, getHmacSecret()); - const p = payload as unknown as AuthPayload; - if (p.type !== 'access') throw new Error('Not an access token'); - return p; - } catch { - throw new UnauthorizedError('Invalid or expired token'); - } -} - -/** - * Require specific roles. Extracts auth first, then checks role. - */ -export async function requireRole( - req: { headers: { authorization?: string } }, - ...roles: string[] -): Promise { - const payload = await extractAuth(req); - if (roles.length > 0 && (!payload.role || !roles.includes(payload.role))) { - throw new ForbiddenError('Insufficient permissions'); - } - return payload; -} +export { extractAuth, requireRole }; diff --git a/backend/src/lib/request-context.ts b/backend/src/lib/request-context.ts index c96c9de..c70cfca 100644 --- a/backend/src/lib/request-context.ts +++ b/backend/src/lib/request-context.ts @@ -1,34 +1,11 @@ /** - * Request-level product context helpers for ChronoMind backend. + * Request-level product context helpers — delegates to @bytelyst/fastify-auth. */ - -import type { FastifyRequest } from 'fastify'; -import { BadRequestError } from '@bytelyst/errors'; - -export interface JwtPayload { - sub: string; - email?: string; - role?: string; - productId?: string; - type?: string; -} - -declare module 'fastify' { - interface FastifyRequest { - jwtPayload?: JwtPayload; - } -} - +import { createRequestContext } from '@bytelyst/fastify-auth'; import { PRODUCT_ID } from './product-config.js'; -export function getRequestProductId(req: FastifyRequest): string { - const jwtPid = req.jwtPayload?.productId; - if (jwtPid && jwtPid !== PRODUCT_ID) { - throw new BadRequestError(`Invalid productId: expected ${PRODUCT_ID}, got ${jwtPid}`); - } - const header = req.headers['x-product-id']; - if (typeof header === 'string' && header.length > 0 && header !== PRODUCT_ID) { - throw new BadRequestError(`Invalid productId: expected ${PRODUCT_ID}, got ${header}`); - } - return PRODUCT_ID; -} +export type { JwtPayload } from '@bytelyst/fastify-auth'; + +const { getRequestProductId, getUserId } = createRequestContext({ productId: PRODUCT_ID }); + +export { getRequestProductId, getUserId };