Files
permit-api/src/auth.ts

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