-
Notifications
You must be signed in to change notification settings - Fork 15
/
Copy pathtoken_handler.ts
314 lines (274 loc) · 12 KB
/
token_handler.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
import { JsonWebKey } from "crypto"
import { Request, Response } from "express"
import jwt, { Algorithm } from "jsonwebtoken"
import jwkToPem from "jwk-to-pem"
import config from "./config"
import * as Lib from "./lib"
import { validateScopesForBulkDataExport, ScopeList } from "./scope"
export default async (req: Request, res: Response) => {
// Require "application/x-www-form-urlencoded" POSTs -----------------------
let ct = req.headers["content-type"] || "";
if (ct.indexOf("application/x-www-form-urlencoded") !== 0) {
return Lib.replyWithOAuthError(res, "invalid_request", {
message: "form_content_type_required"
});
}
// grant_type --------------------------------------------------------------
if (!req.body.grant_type) {
return Lib.replyWithOAuthError(res, "invalid_grant", {
message: "Missing grant_type parameter"
});
}
if (req.body.grant_type != "client_credentials") {
return Lib.replyWithOAuthError(res, "unsupported_grant_type", {
message: "The grant_type parameter should equal 'client_credentials'"
});
}
// client_assertion_type ---------------------------------------------------
if (!req.body.client_assertion_type) {
return Lib.replyWithOAuthError(res, "invalid_request", {
message: "missing_client_assertion_type"
});
}
if (req.body.client_assertion_type != "urn:ietf:params:oauth:client-assertion-type:jwt-bearer") {
return Lib.replyWithOAuthError(res, "invalid_request", {
message: "invalid_client_assertion_type"
});
}
// client_assertion must be a token ----------------------------------------
try {
var authenticationToken = Lib.parseToken(req.body.client_assertion);
} catch (ex) {
return Lib.replyWithOAuthError(res, "invalid_request", {
message: "invalid_registration_token",
params : [ (ex as Error).message ]
});
}
// The client_id must be a token -------------------------------------------
try {
var clientDetailsToken = Lib.parseToken(authenticationToken.sub);
} catch (ex) {
return Lib.replyWithOAuthError(res, "invalid_request", {
message: "invalid_client_details_token",
params : [ (ex as Error).message ]
});
}
// simulate expired_registration_token error -------------------------------
if (clientDetailsToken.err == "token_expired_registration_token") {
return Lib.replyWithOAuthError(res, "invalid_grant", {
message: "token_expired_registration_token"
});
}
// Validate authenticationToken.aud (must equal this url) ------------------
let tokenUrl = config.baseUrl + req.originalUrl;
if (tokenUrl.replace(/^https?/, "") !== authenticationToken.aud.replace(/^https?/, "")) {
return Lib.replyWithOAuthError(res, "invalid_grant", {
message: "invalid_aud",
params: [ tokenUrl ]
});
}
// Validate authenticationToken.iss (must equal whatever the user entered at
// registration time, i.e. clientDetailsToken.iss)
if (authenticationToken.iss && authenticationToken.iss !== authenticationToken.sub) {
return Lib.replyWithOAuthError(res, "invalid_grant", {
message: "invalid_token_iss",
params: [ authenticationToken.iss, authenticationToken.sub ]
});
}
// simulated invalid_jti error ---------------------------------------------
if (clientDetailsToken.err == "invalid_jti") {
return Lib.replyWithOAuthError(res, "invalid_grant", {
message: "invalid_jti"
});
}
// Validate scope ----------------------------------------------------------
// Note that the scope check is FHIR version dependent and makes sure that
// no unknown resources are involved. However, this code is common for every
// FHIR version so we just use "4" here.
let tokenError = await validateScopesForBulkDataExport(req.body.scope, 4);
if (tokenError) {
return res.status(400).json({
error: "invalid_scope",
error_description: tokenError
});
}
// simulated token_invalid_scope -------------------------------------------
if (clientDetailsToken.err == "token_invalid_scope") {
return Lib.replyWithOAuthError(res, "invalid_scope", {
message: "token_invalid_scope"
});
}
// Get the authentication token header
let decodedToken = jwt.decode(req.body.client_assertion, { complete: true, json: true });
if (!decodedToken) {
return Lib.replyWithOAuthError(res, "invalid_client", {
message: "sim_invalid_token",
code : 400
});
}
let header = decodedToken.header;
// Get the "kid" from the authentication token header
let kid = header.kid;
// If the jku header is present, verify that the jku is whitelisted
// (i.e., that it matches the value supplied at registration time for
// the specified `client_id`).
// If the jku header is not whitelisted, the signature verification fails.
if (header.jku && header.jku !== clientDetailsToken.jwks_url) {
return Lib.replyWithOAuthError(res, "invalid_grant", {
message: "jku_not_whitelisted",
params: [ header.jku, clientDetailsToken.jwks_url ]
});
}
// Start a task to fetch the JWKS
Promise.resolve()
.then(() => {
// Case 1: Remote JWKS -------------------------------------------------
// If the jku header is whitelisted, create a set of potential keys
// by dereferencing the jku URL
if (header.jku && clientDetailsToken.jwks_url) {
return Lib.fetchJwks(clientDetailsToken.jwks_url)
.then(json => {
if (!Array.isArray(json.keys)) {
Lib.replyWithOAuthError(res, "invalid_grant", {
message: "The remote jwks object has no keys array."
});
return Promise.reject();
}
return json.keys
}).catch(error => {
Lib.replyWithOAuthError(res, "invalid_client", {
message: "Requesting the remote JWKS returned an error.\n" + error
});
return Promise.reject();
});
}
// Case 2: Remote + local JWKS -----------------------------------------
// If jku is absent, create a set of potential key sources consisting of:
// all keys found by dereferencing the registration-time JWKS URI (if any)
// + any keys supplied in the registration-time JWKS (if any)
if (clientDetailsToken.jwks_url) {
return Lib.fetchJwks(clientDetailsToken.jwks_url)
.then(json => json.keys)
.then(keys => {
// keys supplied in the registration-time JWKS (if any)
if (clientDetailsToken.jwks) {
keys = keys.concat(clientDetailsToken.jwks.keys);
}
return keys;
}).catch(error => {
Lib.replyWithOAuthError(res, "invalid_client", {
message: "Requesting the remote JWKS returned an error.\n" + error
});
return Promise.reject();
});
}
// Case 3: Local JWKS --------------------------------------------------
if (clientDetailsToken.jwks && typeof clientDetailsToken.jwks == "object") {
if (!Array.isArray(clientDetailsToken.jwks.keys)) {
Lib.replyWithOAuthError(res, "invalid_grant", {
message: "The registration-time jwks object has no keys array."
});
return Promise.reject();
}
return clientDetailsToken.jwks.keys;
}
// Case 4: No JWKS -----------------------------------------------------
Lib.replyWithOAuthError(res, "invalid_grant", {
message: "No JWKS found. No 'jku' token header is set, no " +
"registration-time jwks_url is available and no " +
"registration-time jwks is available."
});
return Promise.reject();
})
// Filter the potential keys to retain only those where the `kid` matches
// the value supplied in the client's JWK header.
.then(keys => {
let publicKeys = keys.filter((key: JsonWebKey) => {
if (Array.isArray(key.key_ops) && key.key_ops.indexOf("verify") == -1) {
return false;
}
// return (key.kid === kid && key.kty === header.kty);
return key.kid === kid;
});
if (!publicKeys.length) {
Lib.replyWithOAuthError(res, "invalid_grant", {
message: `No public keys found in the JWKS with "kid" equal to "${kid}"`
});
return Promise.reject();
}
return publicKeys;
})
// Attempt to verify the JWK using each key in the potential keys list.
// - If any attempt succeeds, the signature verification succeeds.
// - If all attempts fail, the signature verification fails.
.then(publicKeys => {
let error = "";
let success = publicKeys.some((key: any) => {
/**
* @type {import("jsonwebtoken").Algorithm}
*/
const algorithm = key.alg;
try {
jwt.verify(
req.body.client_assertion,
jwkToPem(key),
{ algorithms: config.supportedSigningAlgorithms as Algorithm[] }
);
return true;
} catch(ex) {
// console.error(ex);
error = (ex as Error).message;
return false;
}
});
if (!success) {
Lib.replyWithOAuthError(res, "invalid_grant", {
message: "Unable to verify the token with any of the public keys found in the JWKS: " + error
});
return Promise.reject();
}
})
.then(() => {
if (clientDetailsToken.err == "token_invalid_token") {
Lib.replyWithOAuthError(res, "invalid_client", {
message: "sim_invalid_token",
code : 401
});
return Promise.reject();
}
})
.then(() => ScopeList.fromString(req.body.scope).negotiateForExport(4))
.then(grantedScopes => {
if (!grantedScopes.length) {
Lib.replyWithOAuthError(res, "invalid_scope", {
message: `No access could be granted for scopes "${req.body.scope}".`
});
return Promise.reject();
}
// Here, expiresIn is set to the server settings for token lifetime.
// However, if the authentication token has shorter lifetime it will
// also be used for the access token.
const expiresIn = Math.round(Math.min(
authenticationToken.exp - Math.floor(Date.now() / 1000),
clientDetailsToken.accessTokensExpireIn ?
clientDetailsToken.accessTokensExpireIn * 60 :
config.defaultTokenLifeTime * 60
));
var token = Object.assign({}, clientDetailsToken.context, {
token_type: "bearer",
scope : grantedScopes.join(" "),
client_id : req.body.client_id,
expires_in: expiresIn
});
// sim_error
if (clientDetailsToken.err == "request_invalid_token") {
token.err = "Invalid token";
} else if (clientDetailsToken.err == "request_expired_token") {
token.err = "Token expired";
}
// access_token
token.access_token = jwt.sign(token, config.jwtSecret, { expiresIn });
res.json(token);
})
.catch(e => res.end(String(e || "")));
};