refactor(backend): migrate auth.ts + request-context.ts to @bytelyst/fastify-auth

- auth.ts: 80→18 lines, delegates to createAuthMiddleware()
- request-context.ts: delegates to createRequestContext()
- Re-exports JwtPayload, AuthPayload from shared package
This commit is contained in:
saravanakumardb1 2026-03-20 07:45:59 -07:00
parent 2a32a54ae7
commit 942d00cc25
4 changed files with 49 additions and 99 deletions

View File

@ -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

View File

@ -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",

View File

@ -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<typeof createRemoteJWKSet> | null = null;
let jwksUrl: string | undefined;
const { extractAuth, requireRole } = createAuthMiddleware({
jwtSecret: () => config.JWT_SECRET,
jwksUrl: () => config.PLATFORM_JWKS_URL,
});
function getJWKS(): ReturnType<typeof createRemoteJWKSet> | 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<AuthPayload> {
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<AuthPayload> {
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 };

View File

@ -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);
}