150 lines
3.8 KiB
TypeScript
150 lines
3.8 KiB
TypeScript
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<Claim>;
|
|
expiry?: string;
|
|
};
|
|
|
|
declare module "fastify" {
|
|
export interface FastifyRequest {
|
|
user: AuthenticatedUser;
|
|
}
|
|
|
|
export interface FastifyInstance {
|
|
authorize: (req: FastifyRequest, res: FastifyReply) => Promise<unknown>;
|
|
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<Claim>,
|
|
};
|
|
} 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<string>) {
|
|
if (!payload) return;
|
|
|
|
const updatedPayload = JSON.parse(payload);
|
|
|
|
function recursiveDelete(obj: Object | Array<Object>) {
|
|
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);
|
|
}
|