1
1
import { FastifyPluginAsync , FastifyReply , FastifyRequest } from "fastify" ;
2
2
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" ;
6
4
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" ;
7
16
17
+ const CONFIG_SECRET_NAME = "infra-events-api-config" as const ;
8
18
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
+ } ,
11
24
} ;
12
25
13
-
14
26
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
+ }
20
32
}
21
33
return _intersection ;
22
34
}
23
35
24
-
25
- type AadToken = {
36
+ export type AadToken = {
26
37
aud : string ;
27
38
iss : string ;
28
39
iat : number ;
@@ -46,7 +57,29 @@ type AadToken = {
46
57
unique_name : string ;
47
58
uti : string ;
48
59
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
+ } ;
50
83
51
84
const authPlugin : FastifyPluginAsync = async ( fastify , _options ) => {
52
85
fastify . decorate (
@@ -57,57 +90,114 @@ const authPlugin: FastifyPluginAsync = async (fastify, _options) => {
57
90
validRoles : AppRoles [ ] ,
58
91
) : Promise < void > {
59
92
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 ;
66
94
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
+ } ) ;
68
98
}
69
99
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
+ } ) ;
72
104
}
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 ( ) ;
77
155
}
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 ;
84
163
request . username = verifiedTokenData . email ;
85
164
const userRoles = new Set ( [ ] as AppRoles [ ] ) ;
86
- const expectedRoles = new Set ( validRoles )
165
+ const expectedRoles = new Set ( validRoles ) ;
87
166
if ( verifiedTokenData . groups ) {
88
167
for ( const group of verifiedTokenData . groups ) {
89
168
if ( ! GroupRoleMapping [ fastify . runEnvironment ] [ group ] ) {
90
169
continue ;
91
170
}
92
- for ( const role of GroupRoleMapping [ fastify . runEnvironment ] [ group ] ) {
171
+ for ( const role of GroupRoleMapping [ fastify . runEnvironment ] [
172
+ group
173
+ ] ) {
93
174
userRoles . add ( role ) ;
94
175
}
95
176
}
96
177
} 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
+ } ) ;
98
181
}
99
182
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
+ } ) ;
101
186
}
102
187
} catch ( err : unknown ) {
103
188
if ( err instanceof BaseError ) {
104
189
throw err ;
105
190
}
191
+ if ( err instanceof jwt . TokenExpiredError ) {
192
+ throw new UnauthenticatedError ( {
193
+ message : "Token has expired." ,
194
+ } ) ;
195
+ }
106
196
if ( err instanceof Error ) {
107
- request . log . error ( " Failed to verify JWT: " + err . toString ( ) )
197
+ request . log . error ( ` Failed to verify JWT: ${ err . toString ( ) } ` ) ;
108
198
}
109
199
throw new UnauthenticatedError ( {
110
- message : "Could not authenticate from token." ,
200
+ message : "Invalid token." ,
111
201
} ) ;
112
202
}
113
203
} ,
0 commit comments