import bcrypt from 'bcryptjs'; import { FastifyReply, FastifyRequest } from "fastify"; import { getToken } from "./tokens/token.service"; import { Claim } from "./utils/claims"; import { OAuth2Namespace } from "@fastify/oauth2"; import { deleteSession, getSession } from "./auth/auth.service"; import { rules } from "./utils/roles"; import { cacheSession, getCachedSession, removeCachedSession, } from "./utils/cache"; export type AuthenticatedUser = { sid?: string; type: string; userId?: string; orgId?: string; role?: string; tenantId: string; claims: Array; expiry?: string; }; declare module "fastify" { export interface FastifyRequest { user: AuthenticatedUser; } export interface FastifyInstance { authorize: (req: FastifyRequest, res: FastifyReply) => Promise; microsoftOauth: OAuth2Namespace; } export interface FastifyContextConfig { requiredClaims: Claim[]; } } export async function authHandler(req: FastifyRequest, res: FastifyReply) { if (!req.headers.authorization) return res.code(401).send(); const authHeader = req.headers.authorization.split(" ")[1]; if (!authHeader || authHeader == "") return res.code(401).send({ error: "invalid_token" }); if (authHeader.includes(".")) { const [tokenId, token] = authHeader.split("."); if (!tokenId || !token) return res.code(401).send({ error: "invalid_token" }); const tokenInDb = await getToken(tokenId); if (tokenInDb === null) return res.code(401).send({ error: "invalid_token" }); const valid = await bcrypt.compare(token, tokenInDb.hash); if (!valid) return res.code(401).send({ error: "invalid_token" }); req.user = { type: "token", tenantId: tokenInDb.tenantId, claims: tokenInDb.claims as Array, }; } else { let session = getCachedSession(authHeader); if (!session) { session = await getSession(authHeader); cacheSession(authHeader, session); } if (!session) return res.code(401).send({ error: "invalid_token" }); if (new Date() > new Date(session.expiry)) { removeCachedSession(authHeader); await deleteSession(authHeader); return res.code(401).send({ error: "invalid_token" }); } req.user = session; } } export function hasValidClaims( user: AuthenticatedUser, requiredClaims: Claim[] ): boolean { let isValid = true; for (const claim of requiredClaims) { if (!user.claims.includes(claim)) { isValid = false; break; } } return isValid; } export async function authorize(req: FastifyRequest, res: FastifyReply) { const { requiredClaims } = req.routeOptions.config; const authUser = req.user; if (!hasValidClaims(authUser, requiredClaims)) return res .code(401) .send({ error: "Missing permissions", params: requiredClaims }); } export function hideFields(resource: string) { return async function ( req: FastifyRequest, res: FastifyReply, payload: string ) { if (![200, 201].includes(res.statusCode)) return payload; const userRole = req.user.role; if (!userRole) return payload; const hiddenFields = rules[userRole].hiddenFields[resource]; const newRes = deleteFields(payload, hiddenFields); return newRes; }; } function deleteFields(payload: string, hiddenFields: Array) { if (!payload) return; const updatedPayload = JSON.parse(payload); function recursiveDelete(obj: Object | Array) { if (Array.isArray(obj)) { for (const item of obj) { recursiveDelete(item); } } else { for (const key in obj) { if (hiddenFields.includes(key)) { delete obj[key]; } else if (typeof obj[key] == "object" || Array.isArray(obj[key])) { recursiveDelete(obj[key]); } } } } recursiveDelete(updatedPayload); return JSON.stringify(updatedPayload); }