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:
parent
2a32a54ae7
commit
942d00cc25
26
backend/package-lock.json
generated
26
backend/package-lock.json
generated
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user