From 942d00cc25d702cef4dbbf6f8470bf80be020d5f Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Fri, 20 Mar 2026 07:45:59 -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 | 26 ++++++++- backend/package.json | 1 + backend/src/lib/auth.ts | 84 ++++-------------------------- backend/src/lib/request-context.ts | 37 +++++-------- 4 files changed, 49 insertions(+), 99 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index acfb9dc..2887774 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@bytelyst-notes/backend", + "name": "@notelett/backend", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@bytelyst-notes/backend", + "name": "@notelett/backend", "version": "0.1.0", "dependencies": { "@azure/cosmos": "^4.2.0", @@ -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", "@bytelyst/logger": "file:../../learning_ai_common_plat/packages/logger", "fastify": "^5.2.1", @@ -86,6 +87,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", @@ -381,6 +399,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 e145f33..ad87d79 100644 --- a/backend/package.json +++ b/backend/package.json @@ -19,6 +19,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", "@bytelyst/logger": "file:../../learning_ai_common_plat/packages/logger", "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 f556bc9..73d46be 100644 --- a/backend/src/lib/request-context.ts +++ b/backend/src/lib/request-context.ts @@ -1,31 +1,20 @@ +/** + * Request-level product context helpers — delegates to @bytelyst/fastify-auth. + */ +import { createRequestContext } from '@bytelyst/fastify-auth'; import type { FastifyRequest } from 'fastify'; -import { BadRequestError } from '@bytelyst/errors'; import { PRODUCT_ID } from './product-config.js'; -export interface JwtPayload { - sub: string; - email?: string; - role?: string; - productId?: string; - type?: string; -} +export type { JwtPayload } from '@bytelyst/fastify-auth'; -declare module 'fastify' { - interface FastifyRequest { - jwtPayload?: JwtPayload; - } -} +const _ctx = createRequestContext({ productId: PRODUCT_ID }); 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; + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + return _ctx.getRequestProductId(req as any); +} + +export function getUserId(req: FastifyRequest): string { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + return _ctx.getUserId(req as any); }