import { type RequestHandler, type NextFunction, type Request, type Response } from 'express'; import { type FastifyReply, type FastifyRequest } from 'fastify'; import jwt from 'jsonwebtoken'; import { type Resource, type MiauApplication, type JWTToken, issuers, type MiauMetadata } from '@eduzz/miau-types'; import type { MiauClient } from './MiauClient'; class HttpError extends Error { public status: number; constructor(status: number, name: string, message: string, code: string) { super(`${message} (${code})`); this.name = name; this.status = status; Object.setPrototypeOf(this, HttpError.prototype); } } export type RequestAugmentation = (data: { req: Req; app: MiauApplication; meta: T; }) => void; const auth = async ( token: string, method: string, path: string, miauClient: MiauClient ): Promise<[MiauApplication, MiauMetadata]> => { if (!token) { throw new HttpError(400, 'Invalid Token', 'Token not provided', 'MIAU_TKN_A'); } const decodedToken = jwt.decode(token, { complete: true }) as JWTToken | null; if (!decodedToken) { throw new HttpError(400, 'Invalid Token', 'Token could not be decoded', 'MIAU_TKN_B'); } if (!decodedToken.header?.kid) { throw new HttpError(400, 'Invalid Token', 'Missing kid in token header', 'MIAU_TKN_C'); } if (decodedToken.payload.iss !== issuers['production']) { throw new HttpError(400, 'Invalid Token', 'Token issuer is invalid', 'MIAU_TKN_D'); } const publicKey = await miauClient.getPublicKey(decodedToken.header.kid); const clientTokenData = await miauClient.verify(token, publicKey); const serverTokenData = await miauClient.getTokenData(); if ( !clientTokenData || !clientTokenData.application || !clientTokenData.secret || !clientTokenData.application.id || !clientTokenData.secret.id || !clientTokenData.secret.environment ) { throw new HttpError(401, 'Invalid Token', 'Token invalid or expired', 'MIAU_TKN_E'); } const { application: clientApplication, secret: clientSecret } = clientTokenData; const { application: serverApplication, secret: serverSecret } = serverTokenData; if (clientSecret.environment != serverSecret.environment) { throw new HttpError( 401, 'Invalid Environment', `Secret environment ${clientSecret.environment} does not match Server environment ${serverSecret.environment}`, 'MIAU_ENV_A' ); } const resource = { protocol: 'http', method, path } as Resource; const hasPermission = await miauClient.hasPermission(clientApplication.id, resource); if (!hasPermission.success) { throw new HttpError( 403, 'Forbidden', `${clientApplication.name} does not have permission to ${method} ${path} on ${serverApplication.name}`, 'MIAU_PERM_B' ); } const miauApplication = { id: clientApplication.id, name: clientApplication.name }; const miauMetadata = hasPermission.metadata; return [miauApplication, miauMetadata]; }; export const miauMiddleware = ( miauClient: MiauClient, requestAugmentation?: RequestAugmentation, fallbackMiddleware?: RequestHandler ) => { return async (req: Request, res: Response, next: NextFunction) => { try { const token = req.headers.authorization?.split(' ').pop() ?? ''; const fullPath = (req.baseUrl + req.path).replace(/\/+$/, '') || '/'; const [miauApplication, miauMetadata] = await auth(token, req.method, fullPath, miauClient); req.miauApplication = miauApplication; req.miauMetadata = miauMetadata; if (requestAugmentation) { requestAugmentation({ req, app: req.miauApplication, meta: req.miauMetadata as T }); } next(); } catch (err: HttpError | any) { if (err instanceof HttpError && err.status === 400 && fallbackMiddleware) { return fallbackMiddleware(req, res, next); } const errorStatus = err.status || 403; res.status(errorStatus).json({ error: err.name, message: err.message }); return; } }; }; export const miauHook = ( miauClient: MiauClient, requestAugmentation?: RequestAugmentation, fallbackMiddleware?: (request: FastifyRequest, reply: FastifyReply) => void ) => { return async (request: FastifyRequest, reply: FastifyReply) => { try { const token = request.headers.authorization?.split(' ').pop() ?? ''; const path = request.routeOptions.url ?? ''; const [miauApplication, miauMetadata] = await auth(token, request.method, path, miauClient); request.miauApplication = miauApplication; request.miauMetadata = miauMetadata; if (requestAugmentation) { requestAugmentation({ req: request, app: request.miauApplication, meta: request.miauMetadata as T }); } } catch (err: HttpError | any) { if (err instanceof HttpError && err.status === 400 && fallbackMiddleware) { await fallbackMiddleware(request, reply); return; } const errorStatus = err.status || 403; reply.status(errorStatus).send({ error: err.name, message: err.message }); return; } }; };