Skip to content

Commit

Permalink
add authentication support
Browse files Browse the repository at this point in the history
  • Loading branch information
devksingh4 committed Aug 6, 2024
1 parent a085c73 commit 651e3e0
Show file tree
Hide file tree
Showing 10 changed files with 329 additions and 131 deletions.
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"type": "module",
"scripts": {
"build": "rm -rf dist/ && tsc",
"dev": "tsx src/index.ts",
"dev": "tsx watch src/index.ts",
"build:lambda": "yarn build && cp package.json dist/ && yarn lockfile-manage",
"lockfile-manage": "synp --source-file yarn.lock && cp package-lock.json dist/ && rm package-lock.json",
"typecheck": "tsc --noEmit",
Expand All @@ -31,14 +31,15 @@
"eslint-plugin-prettier": "^5.2.1",
"prettier": "^3.3.3",
"synp": "^1.9.13",
"ts-node": "^10.9.2",
"tsx": "^4.16.5",
"typescript": "^5.5.4"
},
"dependencies": {
"@fastify/auth": "^4.6.1",
"@fastify/aws-lambda": "^4.1.0",
"fastify": "^4.28.1",
"fastify-plugin": "^4.5.1"
"fastify-plugin": "^4.5.1",
"jsonwebtoken": "^9.0.2",
"jwks-rsa": "^3.1.0"
}
}
4 changes: 2 additions & 2 deletions src/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ export class UnauthenticatedError extends BaseError<"UnauthenticatedError"> {
}

export class InternalServerError extends BaseError<"InternalServerError"> {
constructor() {
constructor({message}: {message?: string} = {}) {

Check failure on line 50 in src/errors/index.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

Replace `message}:·{message?:·string` with `·message·}:·{·message?:·string·`
super({
name: "InternalServerError",
id: 100,
message: "An internal server error occurred. Please try again or contact support.",
message: message || "An internal server error occurred. Please try again or contact support.",

Check failure on line 54 in src/errors/index.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

Replace `·message·||` with `⏎········message·||⏎·······`
httpStatusCode: 500,
});
}
Expand Down
10 changes: 9 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import FastifyAuthProvider from "@fastify/auth";
import fastifyAuthPlugin from "./plugins/auth.js";
import protectedRoute from "./routes/protected.js";
import errorHandlerPlugin from "./plugins/errorHandler.js";
import { RunEnvironment, runEnvironments } from "./roles.js";
import { InternalServerError } from "./errors/index.js";

const now = () => Date.now();

Expand All @@ -25,7 +27,13 @@ async function init() {
await app.register(fastifyAuthPlugin);
await app.register(FastifyAuthProvider);
await app.register(errorHandlerPlugin);
app.runEnvironment = process.env.RunEnvironment ?? "dev";
if (!process.env.RunEnvironment) {
process.env.RunEnvironment = 'dev';

Check failure on line 31 in src/index.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

Replace `'dev'` with `"dev"`
}
if (!(runEnvironments.includes(process.env.RunEnvironment as RunEnvironment))) {

Check failure on line 33 in src/index.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

Replace `(runEnvironments.includes(process.env.RunEnvironment·as·RunEnvironment)))` with `runEnvironments.includes(process.env.RunEnvironment·as·RunEnvironment))`
throw new InternalServerError({message: `Invalid run environment ${app.runEnvironment}.`})

Check failure on line 34 in src/index.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

Replace `message:·`Invalid·run·environment·${app.runEnvironment}.`})` with `⏎······message:·`Invalid·run·environment·${app.runEnvironment}.`,⏎····});`
}
app.runEnvironment = process.env.RunEnvironment as RunEnvironment;
app.addHook("onRequest", (req, _, done) => {
req.startTime = now();
req.log.info({ url: req.raw.url }, "received request");
Expand Down
117 changes: 94 additions & 23 deletions src/plugins/auth.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,113 @@
import { FastifyPluginAsync, FastifyReply, FastifyRequest } from "fastify";
import fp from "fastify-plugin";
import { BaseError, UnauthenticatedError, UnauthorizedError } from "../errors/index.js";
import { AppRoles } from "../roles.js";
import { BaseError, InternalServerError, UnauthenticatedError, UnauthorizedError } from "../errors/index.js";

Check failure on line 3 in src/plugins/auth.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

Replace `·BaseError,·InternalServerError,·UnauthenticatedError,·UnauthorizedError·` with `⏎··BaseError,⏎··InternalServerError,⏎··UnauthenticatedError,⏎··UnauthorizedError,⏎`
import { AppRoles, RunEnvironment, runEnvironments } from "../roles.js";

Check warning on line 4 in src/plugins/auth.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

'runEnvironments' is defined but never used. Allowed unused vars must match /^_/u
import jwksClient, {JwksClient} from "jwks-rsa";

Check warning on line 5 in src/plugins/auth.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

'JwksClient' is defined but never used. Allowed unused vars must match /^_/u
import jwt, { Algorithm } from "jsonwebtoken";

// const GroupRoleMapping: Record<string, AppRoles[]> = {};
const GroupRoleMapping: Record<RunEnvironment, Record<string, AppRoles[]>> = {
"prod": {"48591dbc-cdcb-4544-9f63-e6b92b067e33": [AppRoles.MANAGER]}, // Infra Chairs
"dev": {"48591dbc-cdcb-4544-9f63-e6b92b067e33": [AppRoles.MANAGER]}, // Infra Chairs
};


function intersection<T>(setA: Set<T>, setB: Set<T>): Set<T> {
let _intersection = new Set<T>();
for (let elem of setB) {
if (setA.has(elem)) {
_intersection.add(elem);
}
}
return _intersection;
}


type AadToken = {
aud: string;
iss: string;
iat: number;
nbf: number;
exp: number;
acr: string;
aio: string;
amr: string[];
appid: string;
appidacr: string;
email: string;
groups?: string[];
idp: string;
ipaddr: string;
name: string;
oid: string;
rh: string;
scp: string;
sub: string;
tid: string;
unique_name: string;
uti: string;
ver: string;
}

const authPlugin: FastifyPluginAsync = async (fastify, _options) => {
fastify.decorate(
"authenticate",
"authorize",
async function (
request: FastifyRequest,
_reply: FastifyReply,
validRoles: AppRoles[],
): Promise<void> {
try {
const clientId = process.env.AadValidClientId;
if (!clientId) {
throw new UnauthenticatedError({message: "Server could not find valid AAD Client ID."})
const AadClientId = process.env.AadValidClientId;
if (!AadClientId) {
request.log.error("Server is misconfigured, could not find `AadValidClientId`!")
throw new InternalServerError({message: "Server authentication is misconfigured, please contact your administrator."});
}
const authHeader = request.headers['authorization']
if (!authHeader) {
throw new UnauthenticatedError({"message": "Did not find bearer token in expected header."})
}
const [method, token] = authHeader.split(" ");
if (method != "Bearer") {
throw new UnauthenticatedError({"message": `Did not find bearer token, found ${method} token.`})
}
const decoded = jwt.decode(token, {complete: true})
const header = decoded?.header;
if (!header) {
throw new UnauthenticatedError({"message": "Could not decode token header."});
}
const verifyOptions = {algorithms: ['RS256' as Algorithm], header: decoded?.header, audience: `api://${AadClientId}`}
const client = jwksClient({
jwksUri: 'https://login.microsoftonline.com/common/discovery/keys'
});
const signingKey = (await client.getSigningKey(header.kid)).getPublicKey();
const verifiedTokenData = jwt.verify(token, signingKey, verifyOptions) as AadToken;
request.username = verifiedTokenData.email;
const userRoles = new Set([] as AppRoles[]);
const expectedRoles = new Set(validRoles)
if (verifiedTokenData.groups) {
for (const group of verifiedTokenData.groups) {
if (!GroupRoleMapping[fastify.runEnvironment][group]) {
continue;
}
for (const role of GroupRoleMapping[fastify.runEnvironment][group]) {
userRoles.add(role);
}
}
} else {
throw new UnauthenticatedError({message: "Could not find groups in token."})
}
if (intersection(userRoles, expectedRoles).size === 0) {
throw new UnauthorizedError({message: "User does not have the privileges for this task."})
}
} catch (err: unknown) {
if (err instanceof BaseError) {
throw err;
}
throw new UnauthenticatedError({ message: "Could not verify JWT." });
}
},
);
fastify.decorate(
"authorize",
async function (
request: FastifyRequest,
_reply: FastifyReply,
_validRoles: AppRoles[],
): Promise<void> {
try {
request.log.info("Authorizing JWT");
} catch (_: unknown) {
throw new UnauthorizedError({
message: "Could not get expected role.",
if (err instanceof Error) {
request.log.error("Failed to verify JWT: " + err.toString())
}
throw new UnauthenticatedError({
message: "Could not authenticate from token.",
});
}
},
Expand Down
1 change: 0 additions & 1 deletion src/plugins/errorHandler.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import fastify, { FastifyReply, FastifyRequest } from 'fastify';

Check warning on line 1 in src/plugins/errorHandler.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

'fastify' is defined but never used. Allowed unused vars must match /^_/u
import fp from 'fastify-plugin';
import { BaseError, InternalServerError, NotFoundError } from '../errors/index.js';
import { request } from 'http';

const errorHandlerPlugin = fp(async(fastify) => {
fastify.setErrorHandler((err: unknown, request: FastifyRequest, reply: FastifyReply) => {
Expand Down
2 changes: 2 additions & 0 deletions src/roles.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/* eslint-disable import/prefer-default-export */
export const runEnvironments = ['dev', 'prod'] as const;
export type RunEnvironment = typeof runEnvironments[number];
export enum AppRoles {
MANAGER,
}
10 changes: 4 additions & 6 deletions src/routes/protected.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,14 @@ const protectedRoute: FastifyPluginAsync = async (fastify, _options) => {
fastify.get(
"/",
{
onRequest: fastify.auth([
fastify.authenticate,
onRequest:
async (request, reply) => {
fastify.authorize(request, reply, [AppRoles.MANAGER]);
},
]),
await fastify.authorize(request, reply, [AppRoles.MANAGER]);
}
},
async (request, reply) => {
reply.send({
message: "hi",
username: request.username,
});
},
);
Expand Down
5 changes: 3 additions & 2 deletions src/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { FastifyRequest, FastifyInstance, FastifyReply } from "fastify";
import { AppRoles } from "./roles.ts";
import { AppRoles, RunEnvironment } from "./roles.ts";
declare module "fastify" {
interface FastifyInstance {
authenticate: (
Expand All @@ -11,9 +11,10 @@ declare module "fastify" {
reply: FastifyReply,
validRoles: AppRoles[],
) => Promise<void>;
runEnvironment: string;
runEnvironment: RunEnvironment;
}
interface FastifyRequest {
startTime: number;
username?: string;
}
}
3 changes: 3 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@
"module": "Node16",
"outDir": "dist",
},
"ts-node": {
"esm": true
},
}
Loading

0 comments on commit 651e3e0

Please sign in to comment.