Skip to content

Commit 6ae8e98

Browse files
committed
enable custom jwt auth
1 parent 651e3e0 commit 6ae8e98

File tree

11 files changed

+1072
-83
lines changed

11 files changed

+1072
-83
lines changed

.eslintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"extends": ["airbnb-base", "plugin:prettier/recommended", "plugin:@typescript-eslint/recommended"],
2+
"extends": ["plugin:prettier/recommended", "plugin:@typescript-eslint/recommended"],
33
"plugins": ["import"],
44
"rules": {
55
// turn on errors for missing imports

generate_jwt.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import jwt from 'jsonwebtoken';
2+
3+
const payload = {
4+
aud: "custom_jwt",
5+
iss: "custom_jwt",
6+
iat: Math.floor(Date.now() / 1000),
7+
nbf: Math.floor(Date.now() / 1000),
8+
exp: Math.floor(Date.now() / 1000) + (3600 * 24), // Token expires after 24 hour
9+
acr: "1",
10+
aio: "AXQAi/8TAAAA",
11+
amr: ["pwd"],
12+
appid: "your-app-id",
13+
appidacr: "1",
14+
15+
groups: ["0"],
16+
idp: "https://login.microsoftonline.com",
17+
ipaddr: "192.168.1.1",
18+
name: "John Doe",
19+
oid: "00000000-0000-0000-0000-000000000000",
20+
rh: "rh-value",
21+
scp: "user_impersonation",
22+
sub: "subject",
23+
tid: "tenant-id",
24+
unique_name: "[email protected]",
25+
uti: "uti-value",
26+
ver: "1.0"
27+
};
28+
29+
const secretKey = process.env.JwtSigningKey;
30+
const token = jwt.sign(payload, secretKey, { algorithm: 'HS256' });
31+
console.log(token)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
"@typescript-eslint/parser": "^8.0.1",
2424
"esbuild": "^0.23.0",
2525
"eslint": "^8.57.0",
26-
"eslint-config-airbnb-base": "^15.0.0",
2726
"eslint-config-esnext": "^4.1.0",
2827
"eslint-config-prettier": "^9.1.0",
2928
"eslint-import-resolver-typescript": "^3.6.1",
@@ -35,6 +34,7 @@
3534
"typescript": "^5.5.4"
3635
},
3736
"dependencies": {
37+
"@aws-sdk/client-secrets-manager": "^3.624.0",
3838
"@fastify/auth": "^4.6.1",
3939
"@fastify/aws-lambda": "^4.1.0",
4040
"fastify": "^4.28.1",

src/errors/index.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ export abstract class BaseError<T extends string> extends Error {
2424
Error.captureStackTrace(this, this.constructor);
2525
}
2626
}
27+
2728
toString() {
28-
return `Error ${this.id} (${this.name}): ${this.message}\n\n${this.stack}`
29+
return `Error ${this.id} (${this.name}): ${this.message}\n\n${this.stack}`;
2930
}
3031
}
3132

@@ -47,19 +48,20 @@ export class UnauthenticatedError extends BaseError<"UnauthenticatedError"> {
4748
}
4849

4950
export class InternalServerError extends BaseError<"InternalServerError"> {
50-
constructor({message}: {message?: string} = {}) {
51+
constructor({ message }: { message?: string } = {}) {
5152
super({
5253
name: "InternalServerError",
5354
id: 100,
54-
message: message || "An internal server error occurred. Please try again or contact support.",
55+
message:
56+
message ||
57+
"An internal server error occurred. Please try again or contact support.",
5558
httpStatusCode: 500,
5659
});
5760
}
5861
}
5962

60-
6163
export class NotFoundError extends BaseError<"NotFoundError"> {
62-
constructor({endpointName}: {endpointName: string}) {
64+
constructor({ endpointName }: { endpointName: string }) {
6365
super({
6466
name: "NotFoundError",
6567
id: 103,

src/index.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@ async function init() {
2828
await app.register(FastifyAuthProvider);
2929
await app.register(errorHandlerPlugin);
3030
if (!process.env.RunEnvironment) {
31-
process.env.RunEnvironment = 'dev';
31+
process.env.RunEnvironment = "dev";
3232
}
33-
if (!(runEnvironments.includes(process.env.RunEnvironment as RunEnvironment))) {
34-
throw new InternalServerError({message: `Invalid run environment ${app.runEnvironment}.`})
33+
if (!runEnvironments.includes(process.env.RunEnvironment as RunEnvironment)) {
34+
throw new InternalServerError({
35+
message: `Invalid run environment ${app.runEnvironment}.`,
36+
});
3537
}
3638
app.runEnvironment = process.env.RunEnvironment as RunEnvironment;
3739
app.addHook("onRequest", (req, _, done) => {

src/plugins/auth.ts

Lines changed: 129 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,39 @@
11
import { FastifyPluginAsync, FastifyReply, FastifyRequest } from "fastify";
22
import fp from "fastify-plugin";
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";
3+
import jwksClient from "jwks-rsa";
64
import jwt, { Algorithm } from "jsonwebtoken";
5+
import {
6+
SecretsManagerClient,
7+
GetSecretValueCommand,
8+
} from "@aws-sdk/client-secrets-manager";
9+
import { AppRoles, RunEnvironment } from "../roles.js";
10+
import {
11+
BaseError,
12+
InternalServerError,
13+
UnauthenticatedError,
14+
UnauthorizedError,
15+
} from "../errors/index.js";
716

17+
const CONFIG_SECRET_NAME = "infra-events-api-config" as const;
818
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
19+
prod: { "48591dbc-cdcb-4544-9f63-e6b92b067e33": [AppRoles.MANAGER] }, // Infra Chairs
20+
dev: {
21+
"48591dbc-cdcb-4544-9f63-e6b92b067e33": [AppRoles.MANAGER], // Infra Chairs
22+
"0": [AppRoles.MANAGER], // Dummy Group for development only
23+
},
1124
};
1225

13-
1426
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-
}
27+
const _intersection = new Set<T>();
28+
for (const elem of setB) {
29+
if (setA.has(elem)) {
30+
_intersection.add(elem);
31+
}
2032
}
2133
return _intersection;
2234
}
2335

24-
25-
type AadToken = {
36+
export type AadToken = {
2637
aud: string;
2738
iss: string;
2839
iat: number;
@@ -46,7 +57,29 @@ type AadToken = {
4657
unique_name: string;
4758
uti: string;
4859
ver: string;
49-
}
60+
};
61+
const smClient = new SecretsManagerClient({
62+
region: process.env.AWS_REGION || "us-east-1",
63+
});
64+
65+
const getSecretValue = async (
66+
secretId: string,
67+
): Promise<Record<string, string | number | boolean> | null> => {
68+
const data = await smClient.send(
69+
new GetSecretValueCommand({ SecretId: secretId }),
70+
);
71+
if (!data.SecretString) {
72+
return null;
73+
}
74+
try {
75+
return JSON.parse(data.SecretString) as Record<
76+
string,
77+
string | number | boolean
78+
>;
79+
} catch {
80+
return null;
81+
}
82+
};
5083

5184
const authPlugin: FastifyPluginAsync = async (fastify, _options) => {
5285
fastify.decorate(
@@ -57,57 +90,114 @@ const authPlugin: FastifyPluginAsync = async (fastify, _options) => {
5790
validRoles: AppRoles[],
5891
): Promise<void> {
5992
try {
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']
93+
const authHeader = request.headers.authorization;
6694
if (!authHeader) {
67-
throw new UnauthenticatedError({"message": "Did not find bearer token in expected header."})
95+
throw new UnauthenticatedError({
96+
message: "Did not find bearer token in expected header.",
97+
});
6898
}
6999
const [method, token] = authHeader.split(" ");
70-
if (method != "Bearer") {
71-
throw new UnauthenticatedError({"message": `Did not find bearer token, found ${method} token.`})
100+
if (method !== "Bearer") {
101+
throw new UnauthenticatedError({
102+
message: `Did not find bearer token, found ${method} token.`,
103+
});
72104
}
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."});
105+
/* eslint-disable @typescript-eslint/no-explicit-any */
106+
const decoded = jwt.decode(token, { complete: true }) as Record<
107+
string,
108+
any
109+
>;
110+
let signingKey = "";
111+
let verifyOptions = {};
112+
if (decoded?.payload.iss === "custom_jwt") {
113+
if (fastify.runEnvironment === "prod") {
114+
throw new UnauthenticatedError({
115+
message: "Custom JWTs cannot be used in Prod environment.",
116+
});
117+
}
118+
signingKey =
119+
process.env.JwtSigningKey ||
120+
(((await getSecretValue(CONFIG_SECRET_NAME)) || { jwt_key: "" })
121+
.jwt_key as string) ||
122+
"";
123+
if (signingKey === "") {
124+
throw new UnauthenticatedError({
125+
message: "Invalid token.",
126+
});
127+
}
128+
verifyOptions = { algorithms: ["HS256" as Algorithm] };
129+
} else {
130+
const AadClientId = process.env.AadValidClientId;
131+
if (!AadClientId) {
132+
request.log.error(
133+
"Server is misconfigured, could not find `AadValidClientId`!",
134+
);
135+
throw new InternalServerError({
136+
message:
137+
"Server authentication is misconfigured, please contact your administrator.",
138+
});
139+
}
140+
const header = decoded?.header;
141+
if (!header) {
142+
throw new UnauthenticatedError({
143+
message: "Could not decode token header.",
144+
});
145+
}
146+
verifyOptions = {
147+
algorithms: ["RS256" as Algorithm],
148+
header: decoded?.header,
149+
audience: `api://${AadClientId}`,
150+
};
151+
const client = jwksClient({
152+
jwksUri: "https://login.microsoftonline.com/common/discovery/keys",
153+
});
154+
signingKey = (await client.getSigningKey(header.kid)).getPublicKey();
77155
}
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;
156+
157+
const verifiedTokenData = jwt.verify(
158+
token,
159+
signingKey,
160+
verifyOptions,
161+
) as AadToken;
162+
request.tokenPayload = verifiedTokenData;
84163
request.username = verifiedTokenData.email;
85164
const userRoles = new Set([] as AppRoles[]);
86-
const expectedRoles = new Set(validRoles)
165+
const expectedRoles = new Set(validRoles);
87166
if (verifiedTokenData.groups) {
88167
for (const group of verifiedTokenData.groups) {
89168
if (!GroupRoleMapping[fastify.runEnvironment][group]) {
90169
continue;
91170
}
92-
for (const role of GroupRoleMapping[fastify.runEnvironment][group]) {
171+
for (const role of GroupRoleMapping[fastify.runEnvironment][
172+
group
173+
]) {
93174
userRoles.add(role);
94175
}
95176
}
96177
} else {
97-
throw new UnauthenticatedError({message: "Could not find groups in token."})
178+
throw new UnauthenticatedError({
179+
message: "Could not find groups in token.",
180+
});
98181
}
99182
if (intersection(userRoles, expectedRoles).size === 0) {
100-
throw new UnauthorizedError({message: "User does not have the privileges for this task."})
183+
throw new UnauthorizedError({
184+
message: "User does not have the privileges for this task.",
185+
});
101186
}
102187
} catch (err: unknown) {
103188
if (err instanceof BaseError) {
104189
throw err;
105190
}
191+
if (err instanceof jwt.TokenExpiredError) {
192+
throw new UnauthenticatedError({
193+
message: "Token has expired.",
194+
});
195+
}
106196
if (err instanceof Error) {
107-
request.log.error("Failed to verify JWT: " + err.toString())
197+
request.log.error(`Failed to verify JWT: ${err.toString()}`);
108198
}
109199
throw new UnauthenticatedError({
110-
message: "Could not authenticate from token.",
200+
message: "Invalid token.",
111201
});
112202
}
113203
},

0 commit comments

Comments
 (0)