Skip to content

Commit 651e3e0

Browse files
committed
add authentication support
1 parent a085c73 commit 651e3e0

File tree

10 files changed

+329
-131
lines changed

10 files changed

+329
-131
lines changed

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"type": "module",
99
"scripts": {
1010
"build": "rm -rf dist/ && tsc",
11-
"dev": "tsx src/index.ts",
11+
"dev": "tsx watch src/index.ts",
1212
"build:lambda": "yarn build && cp package.json dist/ && yarn lockfile-manage",
1313
"lockfile-manage": "synp --source-file yarn.lock && cp package-lock.json dist/ && rm package-lock.json",
1414
"typecheck": "tsc --noEmit",
@@ -31,14 +31,15 @@
3131
"eslint-plugin-prettier": "^5.2.1",
3232
"prettier": "^3.3.3",
3333
"synp": "^1.9.13",
34-
"ts-node": "^10.9.2",
3534
"tsx": "^4.16.5",
3635
"typescript": "^5.5.4"
3736
},
3837
"dependencies": {
3938
"@fastify/auth": "^4.6.1",
4039
"@fastify/aws-lambda": "^4.1.0",
4140
"fastify": "^4.28.1",
42-
"fastify-plugin": "^4.5.1"
41+
"fastify-plugin": "^4.5.1",
42+
"jsonwebtoken": "^9.0.2",
43+
"jwks-rsa": "^3.1.0"
4344
}
4445
}

src/errors/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,11 @@ export class UnauthenticatedError extends BaseError<"UnauthenticatedError"> {
4747
}
4848

4949
export class InternalServerError extends BaseError<"InternalServerError"> {
50-
constructor() {
50+
constructor({message}: {message?: string} = {}) {
5151
super({
5252
name: "InternalServerError",
5353
id: 100,
54-
message: "An internal server error occurred. Please try again or contact support.",
54+
message: message || "An internal server error occurred. Please try again or contact support.",
5555
httpStatusCode: 500,
5656
});
5757
}

src/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import FastifyAuthProvider from "@fastify/auth";
55
import fastifyAuthPlugin from "./plugins/auth.js";
66
import protectedRoute from "./routes/protected.js";
77
import errorHandlerPlugin from "./plugins/errorHandler.js";
8+
import { RunEnvironment, runEnvironments } from "./roles.js";
9+
import { InternalServerError } from "./errors/index.js";
810

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

@@ -25,7 +27,13 @@ async function init() {
2527
await app.register(fastifyAuthPlugin);
2628
await app.register(FastifyAuthProvider);
2729
await app.register(errorHandlerPlugin);
28-
app.runEnvironment = process.env.RunEnvironment ?? "dev";
30+
if (!process.env.RunEnvironment) {
31+
process.env.RunEnvironment = 'dev';
32+
}
33+
if (!(runEnvironments.includes(process.env.RunEnvironment as RunEnvironment))) {
34+
throw new InternalServerError({message: `Invalid run environment ${app.runEnvironment}.`})
35+
}
36+
app.runEnvironment = process.env.RunEnvironment as RunEnvironment;
2937
app.addHook("onRequest", (req, _, done) => {
3038
req.startTime = now();
3139
req.log.info({ url: req.raw.url }, "received request");

src/plugins/auth.ts

Lines changed: 94 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,113 @@
11
import { FastifyPluginAsync, FastifyReply, FastifyRequest } from "fastify";
22
import fp from "fastify-plugin";
3-
import { BaseError, UnauthenticatedError, UnauthorizedError } from "../errors/index.js";
4-
import { AppRoles } from "../roles.js";
3+
import { BaseError, InternalServerError, UnauthenticatedError, UnauthorizedError } from "../errors/index.js";
4+
import { AppRoles, RunEnvironment, runEnvironments } from "../roles.js";
5+
import jwksClient, {JwksClient} from "jwks-rsa";
6+
import jwt, { Algorithm } from "jsonwebtoken";
57

6-
// const GroupRoleMapping: Record<string, AppRoles[]> = {};
8+
const GroupRoleMapping: Record<RunEnvironment, Record<string, AppRoles[]>> = {
9+
"prod": {"48591dbc-cdcb-4544-9f63-e6b92b067e33": [AppRoles.MANAGER]}, // Infra Chairs
10+
"dev": {"48591dbc-cdcb-4544-9f63-e6b92b067e33": [AppRoles.MANAGER]}, // Infra Chairs
11+
};
12+
13+
14+
function intersection<T>(setA: Set<T>, setB: Set<T>): Set<T> {
15+
let _intersection = new Set<T>();
16+
for (let elem of setB) {
17+
if (setA.has(elem)) {
18+
_intersection.add(elem);
19+
}
20+
}
21+
return _intersection;
22+
}
23+
24+
25+
type AadToken = {
26+
aud: string;
27+
iss: string;
28+
iat: number;
29+
nbf: number;
30+
exp: number;
31+
acr: string;
32+
aio: string;
33+
amr: string[];
34+
appid: string;
35+
appidacr: string;
36+
email: string;
37+
groups?: string[];
38+
idp: string;
39+
ipaddr: string;
40+
name: string;
41+
oid: string;
42+
rh: string;
43+
scp: string;
44+
sub: string;
45+
tid: string;
46+
unique_name: string;
47+
uti: string;
48+
ver: string;
49+
}
750

851
const authPlugin: FastifyPluginAsync = async (fastify, _options) => {
952
fastify.decorate(
10-
"authenticate",
53+
"authorize",
1154
async function (
1255
request: FastifyRequest,
1356
_reply: FastifyReply,
57+
validRoles: AppRoles[],
1458
): Promise<void> {
1559
try {
16-
const clientId = process.env.AadValidClientId;
17-
if (!clientId) {
18-
throw new UnauthenticatedError({message: "Server could not find valid AAD Client ID."})
60+
const AadClientId = process.env.AadValidClientId;
61+
if (!AadClientId) {
62+
request.log.error("Server is misconfigured, could not find `AadValidClientId`!")
63+
throw new InternalServerError({message: "Server authentication is misconfigured, please contact your administrator."});
64+
}
65+
const authHeader = request.headers['authorization']
66+
if (!authHeader) {
67+
throw new UnauthenticatedError({"message": "Did not find bearer token in expected header."})
68+
}
69+
const [method, token] = authHeader.split(" ");
70+
if (method != "Bearer") {
71+
throw new UnauthenticatedError({"message": `Did not find bearer token, found ${method} token.`})
72+
}
73+
const decoded = jwt.decode(token, {complete: true})
74+
const header = decoded?.header;
75+
if (!header) {
76+
throw new UnauthenticatedError({"message": "Could not decode token header."});
77+
}
78+
const verifyOptions = {algorithms: ['RS256' as Algorithm], header: decoded?.header, audience: `api://${AadClientId}`}
79+
const client = jwksClient({
80+
jwksUri: 'https://login.microsoftonline.com/common/discovery/keys'
81+
});
82+
const signingKey = (await client.getSigningKey(header.kid)).getPublicKey();
83+
const verifiedTokenData = jwt.verify(token, signingKey, verifyOptions) as AadToken;
84+
request.username = verifiedTokenData.email;
85+
const userRoles = new Set([] as AppRoles[]);
86+
const expectedRoles = new Set(validRoles)
87+
if (verifiedTokenData.groups) {
88+
for (const group of verifiedTokenData.groups) {
89+
if (!GroupRoleMapping[fastify.runEnvironment][group]) {
90+
continue;
91+
}
92+
for (const role of GroupRoleMapping[fastify.runEnvironment][group]) {
93+
userRoles.add(role);
94+
}
95+
}
96+
} else {
97+
throw new UnauthenticatedError({message: "Could not find groups in token."})
98+
}
99+
if (intersection(userRoles, expectedRoles).size === 0) {
100+
throw new UnauthorizedError({message: "User does not have the privileges for this task."})
19101
}
20102
} catch (err: unknown) {
21103
if (err instanceof BaseError) {
22104
throw err;
23105
}
24-
throw new UnauthenticatedError({ message: "Could not verify JWT." });
25-
}
26-
},
27-
);
28-
fastify.decorate(
29-
"authorize",
30-
async function (
31-
request: FastifyRequest,
32-
_reply: FastifyReply,
33-
_validRoles: AppRoles[],
34-
): Promise<void> {
35-
try {
36-
request.log.info("Authorizing JWT");
37-
} catch (_: unknown) {
38-
throw new UnauthorizedError({
39-
message: "Could not get expected role.",
106+
if (err instanceof Error) {
107+
request.log.error("Failed to verify JWT: " + err.toString())
108+
}
109+
throw new UnauthenticatedError({
110+
message: "Could not authenticate from token.",
40111
});
41112
}
42113
},

src/plugins/errorHandler.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import fastify, { FastifyReply, FastifyRequest } from 'fastify';
22
import fp from 'fastify-plugin';
33
import { BaseError, InternalServerError, NotFoundError } from '../errors/index.js';
4-
import { request } from 'http';
54

65
const errorHandlerPlugin = fp(async(fastify) => {
76
fastify.setErrorHandler((err: unknown, request: FastifyRequest, reply: FastifyReply) => {

src/roles.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
/* eslint-disable import/prefer-default-export */
2+
export const runEnvironments = ['dev', 'prod'] as const;
3+
export type RunEnvironment = typeof runEnvironments[number];
24
export enum AppRoles {
35
MANAGER,
46
}

src/routes/protected.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,14 @@ const protectedRoute: FastifyPluginAsync = async (fastify, _options) => {
55
fastify.get(
66
"/",
77
{
8-
onRequest: fastify.auth([
9-
fastify.authenticate,
8+
onRequest:
109
async (request, reply) => {
11-
fastify.authorize(request, reply, [AppRoles.MANAGER]);
12-
},
13-
]),
10+
await fastify.authorize(request, reply, [AppRoles.MANAGER]);
11+
}
1412
},
1513
async (request, reply) => {
1614
reply.send({
17-
message: "hi",
15+
username: request.username,
1816
});
1917
},
2018
);

src/types.d.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { FastifyRequest, FastifyInstance, FastifyReply } from "fastify";
2-
import { AppRoles } from "./roles.ts";
2+
import { AppRoles, RunEnvironment } from "./roles.ts";
33
declare module "fastify" {
44
interface FastifyInstance {
55
authenticate: (
@@ -11,9 +11,10 @@ declare module "fastify" {
1111
reply: FastifyReply,
1212
validRoles: AppRoles[],
1313
) => Promise<void>;
14-
runEnvironment: string;
14+
runEnvironment: RunEnvironment;
1515
}
1616
interface FastifyRequest {
1717
startTime: number;
18+
username?: string;
1819
}
1920
}

tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@
44
"module": "Node16",
55
"outDir": "dist",
66
},
7+
"ts-node": {
8+
"esm": true
9+
},
710
}

0 commit comments

Comments
 (0)