-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathindex.js
448 lines (412 loc) · 14.4 KB
/
index.js
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
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
const querystring = require("querystring");
const constant = require("./common/constant");
const urlParser = require("url");
const requestHandler = require("./lib/requestHandler.js");
const schema = require("./common/config");
const log4js = require("log4js");
const logger = log4js.getLogger("MyInfoNodeJSConnector");
const crypto = require("crypto");
// ####################
// Exporting the Module
// ####################
/**
* MyInfoConnector Constructor
*
* This is a constructor to validate and initialize all the config variables
*
* @param {{
* CLIENT_ID {string},
* SUBENTITY_ID {string},
* REDIRECT_URL {string},
* SCOPE {string},
* AUTHORIZE_JWKS_URL {string},
* MYINFO_JWKS_URL {string},
* TOKEN_URL {string},
* PERSON_URL {string},
* CLIENT_ASSERTION_SIGNING_KID {string},
* USE_PROXY {string},
* PROXY_TOKEN_URL {string},
* PROXY_PERSON_URL {string},
* DEBUG_LEVEL {string},
* }}
*/
class MyInfoConnector {
isInitialized = false;
CONFIG ={}
constructor(config) {
try {
this.CONFIG = config;
logger.level = this.CONFIG.DEBUG_LEVEL;
this.securityHelper = require("./lib/securityHelper");
this.securityHelper.validateSchema(config, schema);
this.isInitialized = true;
} catch (error) {
this.isInitialized = false;
logger.error("Error (Library Init): ", error);
throw error;
}
}
/**
* This method generates the code verifier and code challenge for the PKCE flow.
*
* @returns {Object} - Returns an object consisting of the code verifier and the code challenge
*/
generatePKCECodePair = function () {
try {
let codeVerifier = crypto.randomBytes(32).toString("hex"); //generate a cryptographically strong random string
let codeChallenge = this.securityHelper.base64URLEncode(
//base64url encode the sha256 hash of the codeVerifier
this.securityHelper.sha256(codeVerifier)
);
return {
codeVerifier: codeVerifier,
codeChallenge: codeChallenge,
};
} catch (error) {
logger.error("generateCodeChallenge - Error: ", error);
throw error;
}
};
/**
* Get MyInfo Person Data (MyInfo Token + Person API)
*
* This method takes in all the required variables, invoke the following APIs.
* - Get Access Token (Token API) - to get Access Token by using the Auth Code
* - Get Person Data (Person API) - to get Person Data by using the Access Token
*
* @param {string} authCode - Authorization Code from Authorize API
* @param {string} codeVerifier - Code verifier that corresponds to the code challenge used to retrieve authcode
* @param {string} privateSigningKey - Your application private signing key in .pem format
* @param {Array} privateEncryptionKeys - Your application private encryption keys in .pem format, pass in a list of private keys that corresponds to JWKS encryption public keys
*
* @returns {Promise} - Returns the Person Data (Payload decrypted + Signature validated)
*/
getMyInfoPersonData = async function (
authCode,
codeVerifier,
privateSigningKey,
privateEncryptionKeys
) {
if (!this.isInitialized) {
throw constant.ERROR_UNKNOWN_NOT_INIT;
}
try {
let sessionEphemeralKeyPair =
await this.securityHelper.generateSessionKeyPair(); // Generate a new session Ephemeral Key pair for every request to sign DPoP
//create API call to exchange autcode for access_token
let access_token = await this.getAccessToken(
authCode,
codeVerifier,
sessionEphemeralKeyPair,
privateSigningKey
);
//create API call to exchange access_token to retrieve user's data
let personData = await this.getPersonData(
access_token,
sessionEphemeralKeyPair,
privateEncryptionKeys
);
return personData;
} catch (error) {
throw error;
}
};
/**
* Get Access Token from MyInfo Token API
*
* This method calls the Token API and obtain an "access token",
* which can be used to call the Person API for the actual data.
* Your application needs to provide a valid "authorisation code"
* from the authorize API in exchange for the "access token".
*
* @param {string} authCode - Authorization Code from authorize API
* @param {string} codeVerifier - Code verifier that corresponds to the code challenge used to retrieve authcode
* @param {object} sessionEphemeralKeyPair - Session EphemeralKeyPair used to sign DPoP
* @param {string} privateSigningKey - Your application private signing key in .pem format
* @returns {Promise} - Returns the Access Token
*/
getAccessToken = async function (
authCode,
codeVerifier,
sessionEphemeralKeyPair,
privateSigningKey
) {
if (!this.isInitialized) {
throw constant.ERROR_UNKNOWN_NOT_INIT;
}
try {
let tokenResult = await this.callTokenAPI(
authCode,
privateSigningKey,
codeVerifier,
sessionEphemeralKeyPair
);
logger.debug("Access Token Response: ", tokenResult);
return tokenResult.access_token;
} catch (error) {
logger.error("getAccessToken - Error: ", error);
throw error;
}
};
/**
* Get Person Data from MyInfo Person API
*
* This method calls the Person API and returns a JSON response with the
* personal data that was requested. Your application needs to provide a
* valid "access token" in exchange for the JSON data. Once your application
* receives this JSON data, you can use this data to populate the online
* form on your application.
*
* @param {string} accessToken - Access token from Token API
* @param {object} sessionEphemeralKeyPair - Session EphemeralKeyPair used to sign DPoP
* @param {Array} privateEncryptionKeys - Your application private encryption keys in .pem format, pass in a list of private keys that corresponds to JWKS encryption public keys
* @returns {Promise} Returns the Person Data (Payload decrypted + Signature validated)
*/
getPersonData = async function (
accessToken,
sessionPopKeyPair,
privateEncryptionKeys
) {
if (!this.isInitialized) {
throw constant.ERROR_UNKNOWN_NOT_INIT;
}
try {
let callPersonRequestResult = await this.getPersonDataWithToken(
accessToken,
sessionPopKeyPair,
privateEncryptionKeys
);
return callPersonRequestResult;
} catch (error) {
logger.error("getPersonData - Error: ", error);
throw error;
}
};
/**
* Call (Access) Token API
*
* This method will generate the Token request
* and call the Token API to retrieve access Token
*
* @param {string} authCode - Authorization Code from authorize API
* @param {File} privateSigningKey - The Client Private Key in PEM format
* @param {string} codeVerifier - Code verifier that corresponds to the code challenge used to retrieve authcode
* @param {object} sessionEphemeralKeyPair - Session EphemeralKeyPair used to sign DPoP
*
* @returns {Promise} - Returns the Access Token
*/
callTokenAPI = async function (
authCode,
privateSigningKey,
codeVerifier,
sessionEphemeralKeyPair
) {
let cacheCtl = "no-cache";
let contentType = "application/x-www-form-urlencoded";
let method = constant.HTTP_METHOD.POST;
let clientAssertionType =
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
//generate jkt thumbprint from Ephemeral public key
let jktThumbprint = await this.securityHelper.generateJwkThumbprint(
sessionEphemeralKeyPair.publicKey
);
let strParams;
// assemble params for Token API
strParams =
`grant_type=authorization_code` +
"&code=" + //grant type is 'code' for authorization_code flow
authCode +
"&redirect_uri=" +
this.CONFIG.REDIRECT_URL + // redirect url should match redirect url used in /authorise call
"&client_id=" +
this.CONFIG.CLIENT_ID +
"&code_verifier=" +
codeVerifier +
"&client_assertion_type=" +
clientAssertionType +
"&client_assertion=" +
(await this.securityHelper.generateClientAssertion(
this.CONFIG.TOKEN_URL,
this.CONFIG.CLIENT_ID,
privateSigningKey,
jktThumbprint,
this.CONFIG.CLIENT_ASSERTION_SIGNING_KID
));
//generate corresponding DPoP for client_assertion
let dPoP = await this.securityHelper.generateDpop(
this.CONFIG.TOKEN_URL,
null,
constant.HTTP_METHOD.POST,
sessionEphemeralKeyPair
);
// assemble headers for Token API
let strHeaders = `Content-Type=${contentType}&Cache-Control=${cacheCtl}&DPoP=${dPoP}`;
let headers = querystring.parse(strHeaders);
// invoke Token API
let tokenURL =
this.CONFIG.USE_PROXY && this.CONFIG.USE_PROXY == "Y"
? this.CONFIG.PROXY_TOKEN_URL
: this.CONFIG.TOKEN_URL;
let accessToken = await requestHandler.getHttpsResponse(
method,
tokenURL,
headers,
strParams,
null
);
return accessToken.data;
};
/**
* Call Person API
*
* This method will generate the Person request and
* call the Person API to get the encrypted Person Data
*
* @param {string} sub - The retrieved uuid sub from the decoded access_token
* @param {string} accessToken - The encoded access_token from /token API
* @param {object} sessionEphemeralKeyPair - Session EphemeralKeyPair used to sign DPoP
*
* @returns {Promise} Returns result from calling Person API
*/
callPersonAPI = async function (sub, accessToken, sessionEphemeralKeyPair) {
let urlLink;
//Code to handle Myinfo Biz Entity Person URL
if (this.CONFIG.PERSON_URL.includes('biz')) {
let subTemp = sub.split('_');
var uen = subTemp[0];
var uuid = subTemp[1];
urlLink = this.CONFIG.PERSON_URL + '/' + uen + '/' + uuid;
} else {
urlLink = this.CONFIG.PERSON_URL + '/' + sub;
}
let cacheCtl = "no-cache";
let method = constant.HTTP_METHOD.GET;
// assemble params for Person API
let strParams = "scope=" + encodeURIComponent(this.CONFIG.SCOPE);
//append subentity if configured
if (this.CONFIG.SUBENTITY_ID) {
strParams = `${strParams}&subentity=${this.CONFIG.SUBENTITY_ID}`;
}
// assemble headers for Person API
let strHeaders = "Cache-Control=" + cacheCtl;
let headers = querystring.parse(strHeaders);
//generate ath to append into DPoP
let ath = this.securityHelper.base64URLEncode(
this.securityHelper.sha256(accessToken)
);
//generate DPoP
let dpopToken = await this.securityHelper.generateDpop(
urlLink,
ath,
method,
sessionEphemeralKeyPair
);
headers["dpop"] = dpopToken;
headers["Authorization"] = "DPoP " + accessToken;
logger.info(
"Authorization Header for MyInfo Person API: ",
JSON.stringify(headers)
);
// invoke person API
let personURL =
this.CONFIG.USE_PROXY && this.CONFIG.USE_PROXY == "Y"
? this.CONFIG.PROXY_PERSON_URL
: this.CONFIG.PERSON_URL;
let parsedUrl = urlParser.parse(personURL);
let domain = parsedUrl.hostname;
//update url to include uen for Myinfo Biz
let requestPath = this.CONFIG.PERSON_URL.includes('biz') ? `${parsedUrl.path}/${uen}/${uuid}?${strParams}` :
`${parsedUrl.path}/${sub}?${strParams}`;
//invoking https to do GET call
let personData = await requestHandler.getHttpsResponse(
method,
"https://" + domain + requestPath,
headers,
null,
null
);
return personData.data;
};
/**
* Get Person Data
*
* This method will take in the accessToken from Token API and decode it
* to get the sub(eg either uinfin or uuid). It will call the Person API using the token and sub.
* It will verify the Person API data's signature and decrypt the result.
*
* @param {string} accessToken - The encoded token that was returned from /token API
* @param {object} sessionEphemeralKeyPair - Session EphemeralKeyPair used to sign DPoP
* @param {Array} privateEncryptionKeys - Your application private encryption keys in .pem format, pass in a list of private keys that corresponds to JWKS encryption public keys
* @returns {Promise} Returns decrypted result from calling Person API
*/
getPersonDataWithToken = async function (
accessToken,
sessionEphemeralKeyPair,
privateEncryptionKeys
) {
try {
//decode and verify token
let decodedToken = await this.securityHelper.verifyJWS(
accessToken,
this.CONFIG.AUTHORIZE_JWKS_URL
);
logger.debug(
"Decoded Access Token (from MyInfo Token API): ",
decodedToken
);
if (!decodedToken) {
logger.error("Error: ", constant.ERROR_INVALID_TOKEN);
throw constant.ERROR_INVALID_TOKEN;
}
let uinfin = decodedToken.sub;
if (!uinfin) {
logger.error("Error: ", constant.ERROR_UINFIN_NOT_FOUND);
throw constant.ERROR_UINFIN_NOT_FOUND;
}
let personResult;
personResult = await this.callPersonAPI(
uinfin,
accessToken,
sessionEphemeralKeyPair
);
let decryptedResponse;
if (personResult) {
logger.debug("MyInfo PersonAPI Response (JWE+JWS): ", personResult);
//Test decryption with different keys passed in (in the event that multiple enc keys configured on JWKS)
for (let i = 0; i < privateEncryptionKeys.length; i++) {
let jws = await this.securityHelper.decryptJWEWithKey(
personResult,
privateEncryptionKeys[i]
);
if (jws != constant.ERROR_DECRYPT_JWE) {
logger.debug("Decrypted JWE: ", jws);
decryptedResponse = jws;
break;
}
}
} else {
logger.error("Error: ", constant.ERROR);
throw constant.ERROR;
}
let decodedData;
if (!decryptedResponse) {
logger.error("Error: ", constant.ERROR_INVALID_DATA_OR_SIGNATURE);
throw constant.ERROR_INVALID_DATA_OR_SIGNATURE;
}
//verify the signature of the decrypted JWS
decodedData = await this.securityHelper.verifyJWS(
decryptedResponse,
this.CONFIG.MYINFO_JWKS_URL
);
// successful. return data back to frontend
logger.debug(
"Person Data (JWE Decrypted + JWS Verified): ",
JSON.stringify(decodedData)
);
return decodedData;
} catch (error) {
throw error;
}
};
}
module.exports = MyInfoConnector;