@connectid-tools/rp-nodejs-sdk 4.2.1 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +64 -71
  2. package/config.js +2 -31
  3. package/conformance/api/conformance-api.d.ts +38 -0
  4. package/conformance/api/conformance-api.js +53 -0
  5. package/conformance/conformance-config.d.ts +2 -0
  6. package/conformance/conformance-config.js +34 -0
  7. package/crypto/crypto-loader.d.ts +32 -0
  8. package/crypto/crypto-loader.js +49 -0
  9. package/crypto/jwt-helper.d.ts +61 -0
  10. package/crypto/jwt-helper.js +92 -0
  11. package/crypto/pkce-helper.d.ts +43 -0
  12. package/crypto/pkce-helper.js +75 -0
  13. package/endpoints/participants-endpoint.d.ts +55 -0
  14. package/endpoints/participants-endpoint.js +137 -0
  15. package/endpoints/pushed-authorisation-request-endpoint.d.ts +87 -0
  16. package/endpoints/pushed-authorisation-request-endpoint.js +192 -0
  17. package/endpoints/retrieve-token-endpoint.d.ts +66 -0
  18. package/endpoints/retrieve-token-endpoint.js +159 -0
  19. package/endpoints/userinfo-endpoint.d.ts +24 -0
  20. package/endpoints/userinfo-endpoint.js +50 -0
  21. package/fapi/fapi-utils.d.ts +6 -0
  22. package/fapi/fapi-utils.js +9 -0
  23. package/http/http-client-extensions.d.ts +60 -0
  24. package/http/http-client-extensions.js +106 -0
  25. package/http/http-client-factory.d.ts +27 -0
  26. package/http/http-client-factory.js +45 -0
  27. package/model/callback-params.d.ts +31 -0
  28. package/model/callback-params.js +1 -0
  29. package/model/claims.d.ts +100 -0
  30. package/model/claims.js +1 -0
  31. package/model/consolidated-token-set.d.ts +74 -0
  32. package/model/consolidated-token-set.js +100 -0
  33. package/model/discovery-service.d.ts +46 -0
  34. package/model/discovery-service.js +112 -0
  35. package/model/issuer-metadata.d.ts +165 -0
  36. package/model/issuer-metadata.js +1 -0
  37. package/model/jwks.d.ts +12 -0
  38. package/model/jwks.js +1 -0
  39. package/model/token-response.d.ts +31 -0
  40. package/model/token-response.js +1 -0
  41. package/model/token-set.d.ts +73 -0
  42. package/model/token-set.js +179 -0
  43. package/package.json +4 -5
  44. package/relying-party-client-sdk.d.ts +55 -24
  45. package/relying-party-client-sdk.js +90 -304
  46. package/test-data/large-participants-test-data.d.ts +865 -0
  47. package/test-data/large-participants-test-data.js +18907 -0
  48. package/test-data/participants-test-data.d.ts +149 -0
  49. package/test-data/participants-test-data.js +458 -0
  50. package/test-data/sandbox-participants-test-data.d.ts +865 -0
  51. package/test-data/sandbox-participants-test-data.js +3794 -0
  52. package/types.d.ts +61 -32
  53. package/utils/request-utils.d.ts +1 -1
  54. package/utils/request-utils.js +5 -5
  55. package/utils/user-agent.d.ts +1 -1
  56. package/utils/user-agent.js +1 -1
@@ -1,31 +1,19 @@
1
- // Simple API wrapper over the node-openid-client that provides utility methods
2
- // supporting the specific implementation required for the ConnectID Digital Identity
3
- // solution.
4
- //
5
- // The intent is the Relying Parties can use this as a simple SDK for interacting
6
- // with the ConnectID solution.
7
- import { exportJWK } from 'jose';
8
- import { createPrivateKey, randomUUID } from 'crypto';
9
- import { globalAgent } from 'node:https';
10
- import { rootCertificates } from 'node:tls';
11
- import { custom, Issuer } from 'openid-client';
12
1
  import { getCertificate } from './utils/cert-utils.js';
13
- import { partition } from './utils/functional-utils.js';
14
2
  import { getLogger } from './logger.js';
15
3
  import ParticipantFilters from './filter/participant-filters.js';
16
4
  import { illegalPurposeChars, isValidCertificate, validatePurpose } from './validator.js';
17
- import { generatePushAuthorisationRequestParams } from './utils/request-utils.js';
18
- import { buildUserAgent } from './utils/user-agent.js';
19
- // The extended list of claims which can be requested for a user
20
- const extendedClaimList = ['over16', 'over18', 'over21', 'over25', 'over65', 'beneficiary_account_au', 'beneficiary_account_au_payid', 'beneficiary_account_international', 'cba_loyalty'];
5
+ import { CryptoLoader } from './crypto/crypto-loader.js';
6
+ import { JwtHelper } from './crypto/jwt-helper.js';
7
+ import { HttpClientFactory } from './http/http-client-factory.js';
8
+ import { ParticipantsEndpoint } from './endpoints/participants-endpoint.js';
9
+ import { PushedAuthorisationRequestEndpoint } from './endpoints/pushed-authorisation-request-endpoint.js';
10
+ import { RetrieveTokenEndpoint } from './endpoints/retrieve-token-endpoint.js';
11
+ import { UserInfoEndpoint } from './endpoints/userinfo-endpoint.js';
21
12
  export default class RelyingPartyClientSdk {
22
13
  constructor(config) {
23
14
  this.purpose = 'verifying your identity';
24
- this.participantFilters = new ParticipantFilters();
25
- this.default_cache_ttl = 600;
26
- this.cachedParticipantsExpiry = 0;
27
- this.cachedParticipants = [];
28
15
  this.config = config;
16
+ // Validate certificates
29
17
  if (!isValidCertificate(this.config.data.transport_key, this.config.data.transport_key_content)) {
30
18
  throw new Error('Either transport_key or transport_key_content must be provided');
31
19
  }
@@ -38,30 +26,28 @@ export default class RelyingPartyClientSdk {
38
26
  if (!isValidCertificate(this.config.data.ca_pem, this.config.data.ca_pem_content)) {
39
27
  throw new Error('Either ca_pem or ca_pem_content must be provided');
40
28
  }
41
- this.transportKey = getCertificate(this.config.data.transport_key, this.config.data.transport_key_content);
42
- this.transportPem = getCertificate(this.config.data.transport_pem, this.config.data.transport_pem_content);
43
- this.signingKey = getCertificate(this.config.data.signing_key, this.config.data.signing_key_content);
44
- this.caPem = getCertificate(this.config.data.ca_pem, this.config.data.ca_pem_content);
45
29
  this.logger = getLogger(this.config.data.log_level);
46
- this.logger.info(`Creating RelyingPartyClientSdk - version 4.2.1`);
30
+ this.logger.info(`Creating RelyingPartyClientSdk - version 5.0.1`);
31
+ // Validate and set purpose
47
32
  if (this.config.data.purpose) {
48
33
  const purposeValidation = validatePurpose(this.config.data.purpose);
49
34
  if (purposeValidation === 'INVALID_LENGTH') {
50
35
  this.logger.warn('Purpose must be between 3 and 300 characters');
51
- throw new Error(`Invalid purpose for supplied in config: ${this.config.data.purpose}`);
36
+ throw new Error(`Invalid purpose supplied in config: ${this.config.data.purpose}`);
52
37
  }
53
38
  if (purposeValidation === 'INVALID_CHARACTERS') {
54
39
  this.logger.warn(`Purpose cannot contain any of the following characters: ${illegalPurposeChars.join(',')}, purpose supplied: [${this.config.data.purpose}]`);
55
- throw new Error(`Invalid purpose for supplied in config: ${this.config.data.purpose}`);
40
+ throw new Error(`Invalid purpose supplied in config: ${this.config.data.purpose}`);
56
41
  }
57
42
  this.purpose = this.config.data.purpose;
58
43
  this.logger.info(`Using default purpose supplied in config: ${this.purpose}`);
59
44
  }
60
45
  else {
61
- this.logger.info(`Using built in default purpose: ${this.purpose}`);
46
+ this.logger.info(`Using built-in default purpose: ${this.purpose}`);
62
47
  }
48
+ // Log filtering configuration
63
49
  if (this.config.data.include_uncertified_participants) {
64
- this.logger.info(`Identity provider list will not be filtered as include_uncertified_participants=true`);
50
+ this.logger.info('Identity provider list will not be filtered as include_uncertified_participants=true');
65
51
  }
66
52
  else {
67
53
  if (this.config.data.required_claims) {
@@ -71,294 +57,94 @@ export default class RelyingPartyClientSdk {
71
57
  this.logger.info(`Identity provider list will be filtered for participants that support the following certifications: ${JSON.stringify(this.config.data.required_participant_certifications)}`);
72
58
  }
73
59
  }
74
- globalAgent.options.cert = this.transportPem;
75
- globalAgent.options.key = this.transportKey;
76
- globalAgent.options.ca = [this.caPem, ...rootCertificates];
77
- custom.setHttpOptionsDefaults({ timeout: 10000 });
78
- // 4.2.1 is replaced with `postbuild` script in package.json (see replace-in-files)
60
+ // Log certificate source
79
61
  this.logger.info(`Using ${this.config.data.transport_key_content ? 'transport_key_content' : 'transport_key'} config prop`);
80
62
  this.logger.info(`Using ${this.config.data.transport_pem_content ? 'transport_pem_content' : 'transport_pem'} config prop`);
81
63
  this.logger.info(`Using ${this.config.data.ca_pem_content ? 'ca_pem_content' : 'ca_pem'} config prop`);
82
64
  this.logger.info(`Using ${this.config.data.signing_key_content ? 'signing_key_content' : 'signing_key'} config prop`);
83
- }
84
- // Get the list of Participating DPs within the scheme and their metadata
65
+ // Initialize crypto
66
+ const signingKeyObject = CryptoLoader.loadPrivateKey(getCertificate(this.config.data.signing_key, this.config.data.signing_key_content));
67
+ // Initialize JWT helper
68
+ this.jwtHelper = new JwtHelper(signingKeyObject, this.config.data.signing_kid, this.config.data.client_id);
69
+ // Initialize HTTP client
70
+ this.httpClient = HttpClientFactory.createClient({
71
+ transportKey: getCertificate(this.config.data.transport_key, this.config.data.transport_key_content),
72
+ transportPem: getCertificate(this.config.data.transport_pem, this.config.data.transport_pem_content),
73
+ caPem: getCertificate(this.config.data.ca_pem, this.config.data.ca_pem_content),
74
+ clientId: this.config.data.client_id,
75
+ });
76
+ // Initialize endpoints
77
+ this.participantsEndpoint = new ParticipantsEndpoint(this.config, new ParticipantFilters(), this.httpClient, this.logger, () => this.getCurrentDate());
78
+ this.pushedAuthorisationRequestEndpoint = new PushedAuthorisationRequestEndpoint(this.config, this.httpClient, this.jwtHelper, this.logger, this.participantsEndpoint);
79
+ this.retrieveTokenEndpoint = new RetrieveTokenEndpoint(this.config, this.httpClient, this.jwtHelper, this.logger, this.participantsEndpoint);
80
+ this.userInfoEndpoint = new UserInfoEndpoint(this.httpClient, this.logger, this.config.data.client_id, this.participantsEndpoint);
81
+ }
82
+ /**
83
+ * Get the list of participating identity providers within the scheme.
84
+ *
85
+ * Applies filtering based on SDK configuration.
86
+ *
87
+ * @returns List of participants
88
+ */
85
89
  async getParticipants() {
86
- const participantsUri = this.config.data.registry_participants_uri;
87
- try {
88
- const participants = await this.retrieveFullParticipantsList(participantsUri);
89
- this.logger.info(`Retrieved identity providers from ${participantsUri}, num orgs found: ${participants.length}`);
90
- let filteredParticipants = this.participantFilters.removeFallbackIdentityServiceProvider(participants);
91
- if (this.config.data.include_uncertified_participants) {
92
- this.logger.info(`Identity provider list has not been filtered as include_uncertified_participants=true`);
93
- filteredParticipants = this.participantFilters.removeParticipantsWithoutAuthServers(filteredParticipants);
94
- return filteredParticipants;
95
- }
96
- filteredParticipants = this.participantFilters.removeOutOfDateCertifications(filteredParticipants, this.getCurrentDate());
97
- filteredParticipants = this.participantFilters.removeUnofficialCertifications(filteredParticipants);
98
- if (this.config.data.required_claims) {
99
- this.logger.debug(`Identity provider list filtered for participants that support the following claims: ${JSON.stringify(this.config.data.required_claims)}`);
100
- filteredParticipants = this.participantFilters.filterAuthServersForSupportedClaims(filteredParticipants, this.config.data.required_claims);
101
- }
102
- if (this.config.data.required_participant_certifications) {
103
- this.logger.debug(`Identity provider list filtered for participants that support the following certifications: ${JSON.stringify(this.config.data.required_participant_certifications)}`);
104
- filteredParticipants = this.participantFilters.filterForRequiredCertifications(filteredParticipants, this.config.data.required_participant_certifications);
105
- }
106
- filteredParticipants = this.participantFilters.removeInactiveAuthServers(filteredParticipants);
107
- filteredParticipants = this.participantFilters.removeParticipantsWithoutAuthServers(filteredParticipants);
108
- return filteredParticipants;
109
- }
110
- catch (e) {
111
- throw new Error(`Unable to get participants from ${participantsUri}, ${e}`);
112
- }
90
+ return this.participantsEndpoint.getParticipants();
113
91
  }
92
+ /**
93
+ * Get the list of fallback provider participants.
94
+ *
95
+ * @returns List of fallback provider participants
96
+ */
114
97
  async getFallbackProviderParticipants() {
115
- const participantsUri = this.config.data.registry_participants_uri;
116
- try {
117
- const participants = await this.retrieveFullParticipantsList(participantsUri);
118
- this.logger.info(`Retrieved identity providers, num orgs found: ${participants.length}`);
119
- let filteredParticipants = this.participantFilters.removeOutOfDateCertifications(participants, this.getCurrentDate());
120
- filteredParticipants = this.participantFilters.removeUnofficialCertifications(filteredParticipants);
121
- filteredParticipants = this.participantFilters.filterForFallbackIdentityServiceProviders(filteredParticipants);
122
- filteredParticipants = this.participantFilters.removeParticipantsWithoutAuthServers(filteredParticipants);
123
- return filteredParticipants;
124
- }
125
- catch (e) {
126
- throw new Error(`Unable to get participants from ${participantsUri}, ${e}`);
127
- }
128
- }
129
- // This method is public, so we can mock the current date in tests
130
- getCurrentDate() {
131
- return new Date();
132
- }
133
- async fetchParticipants(participantsUri) {
134
- const response = await fetch(participantsUri, {
135
- headers: {
136
- 'User-Agent': buildUserAgent(this.config.data.client.client_id),
137
- },
138
- });
139
- if (!response.ok) {
140
- throw new Error(`Failed to retrieve participants from ${participantsUri}: status (${response.status})`);
141
- }
142
- return await response.json();
143
- }
144
- async retrieveFullParticipantsList(participantsUri) {
145
- const currentTime = Date.now();
146
- if (currentTime > this.cachedParticipantsExpiry) {
147
- this.cachedParticipants = await this.fetchParticipants(participantsUri);
148
- this.cachedParticipantsExpiry = currentTime + (this.config.data.cache_ttl ?? this.default_cache_ttl) * 1000;
149
- }
150
- // ensure the cached value remain untouched down the call stack by returning a deep copy
151
- return this.cachedParticipants.map((participant) => Object.assign({}, participant));
152
- }
153
- // Create and send a pushed authorisation request to the specified authorisation
154
- // server to allow the initiation of an OIDC flow.
98
+ return this.participantsEndpoint.getFallbackProviderParticipants();
99
+ }
100
+ /**
101
+ * Sends a Pushed Authorization Request (PAR).
102
+ *
103
+ * @param authServerId - Authorization server ID
104
+ * @param essentialClaims - Claims that must be provided
105
+ * @param voluntaryClaims - Claims that are optional
106
+ * @param purpose - Purpose string for data sharing
107
+ * @returns Object containing authorization URL and PKCE parameters
108
+ */
155
109
  async sendPushedAuthorisationRequest(authServerId, essentialClaims, voluntaryClaims = [], purpose = this.purpose) {
156
- const purposeValidation = validatePurpose(purpose);
157
- if (purposeValidation === 'INVALID_LENGTH') {
158
- this.logger.warn('Purpose must be between 3 and 300 characters');
159
- throw new Error(`Invalid purpose for supplied for PAR: ${purpose}`);
160
- }
161
- if (purposeValidation === 'INVALID_CHARACTERS') {
162
- this.logger.warn(`Purpose cannot contain any of the following characters: ${illegalPurposeChars.join(',')}, purpose supplied: [${purpose}]`);
163
- throw new Error(`Invalid purpose for supplied for PAR: ${purpose}`);
164
- }
165
- const xFapiInteractionId = this.generateXFapiInteractionId();
166
- try {
167
- const essentialClaimsWithTxn = [...new Set([...essentialClaims, 'txn'])];
168
- const claimsRequest = this.generateClaimsRequest(essentialClaimsWithTxn, voluntaryClaims);
169
- const authServer = await this.getAuthServerDetails(authServerId);
170
- const { fapiClient } = await this.setupClient(authServer, xFapiInteractionId);
171
- const { authUrl, code_verifier, state, nonce } = await this.generateRequest(fapiClient, claimsRequest, purpose);
172
- this.logger.info(`Sent PAR to auth server: ${authServerId} - ${authServer.CustomerFriendlyName}, essential claims: ${essentialClaimsWithTxn}, voluntary claims: ${voluntaryClaims}, x-fapi-interaction-id: ${xFapiInteractionId}`);
173
- return { authUrl, code_verifier, state, nonce, xFapiInteractionId };
174
- }
175
- catch (e) {
176
- this.logger.error(`Error sending pushed authorisation request to authorisation server ${authServerId}, x-fapi-interaction-id: ${xFapiInteractionId}.`, e);
177
- throw new Error(`Unable to send pushed authorisation request to ${authServerId}, x-fapi-interaction-id: ${xFapiInteractionId} - ${e}`);
178
- }
179
- }
180
- // Process the callback response and return the tokens (access token and id token as a token set)
181
- async retrieveTokens(authorisationServerId, requestParams, codeVerifier, state, nonce) {
182
- const xFapiInteractionId = this.generateXFapiInteractionId();
183
- try {
184
- const authServer = await this.getAuthServerDetails(authorisationServerId);
185
- const { fapiClient, localIssuer } = await this.setupClient(authServer, xFapiInteractionId);
186
- const tokenSet = await fapiClient.callback(this.config.data.application_redirect_uri, requestParams, { code_verifier: codeVerifier, state, nonce, response_type: 'code' }, { clientAssertionPayload: { aud: localIssuer.metadata.issuer } });
187
- this.logger.info(`Retrieved tokenSet from auth server: ${authorisationServerId} - ${authServer.CustomerFriendlyName}, x-fapi-interaction-id: ${xFapiInteractionId}, txn: ${tokenSet.claims().txn}`);
188
- this.logger.debug(`Tokens returned from: ${authorisationServerId} - ${authServer.CustomerFriendlyName}: ${JSON.stringify(tokenSet)} claims: ${JSON.stringify(tokenSet.claims())}`);
189
- // If using the OIDF test suite, need to make a call to userInfo when claims were successfully decoded
190
- if (this.config.data.enable_auto_compliance_verification) {
191
- this.logger.info('Auto compliance verification mode enabled, making call to userInfo since claims successfully decoded');
192
- if (tokenSet.access_token != null) {
193
- await this.getUserInfo(authorisationServerId, tokenSet.access_token);
194
- }
195
- }
196
- return this.buildConsolidatedTokenSet(tokenSet, xFapiInteractionId);
197
- }
198
- catch (e) {
199
- this.logger.error(`Error retrieving tokens with authorisation server ${authorisationServerId}, x-fapi-interaction-id: ${xFapiInteractionId}`, e);
200
- throw new Error(`Unable to retrieve tokens with authorisation server ${authorisationServerId}, x-fapi-interaction-id: ${xFapiInteractionId}`);
201
- }
202
- }
203
- buildConsolidatedTokenSet(tokenSet, xFapiInteractionId) {
110
+ const { authUrl, codeVerifier, state, nonce, xFapiInteractionId } = await this.pushedAuthorisationRequestEndpoint.sendPushedAuthorisationRequest(authServerId, essentialClaims, voluntaryClaims, purpose);
204
111
  return {
205
- ...tokenSet,
112
+ authUrl,
113
+ codeVerifier,
114
+ state,
115
+ nonce,
206
116
  xFapiInteractionId,
207
- expired: () => tokenSet.expired(),
208
- claims: () => tokenSet.claims(),
209
- // @ts-ignore IdTokenClaims does not have `verified_claims` property (unknown)
210
- consolidatedClaims: () => ({ ...tokenSet.claims(), ...tokenSet.claims().verified_claims?.claims }),
211
117
  };
212
118
  }
213
- // Get the full details of an authorisation server given its AuthorisationServerId
214
- async getAuthServerDetails(authServerId) {
215
- const idps = await this.getParticipants();
216
- const servers = idps.map(({ AuthorisationServers }) => AuthorisationServers).flat();
217
- let found = servers.find(({ AuthorisationServerId }) => AuthorisationServerId === authServerId);
218
- if (!found) {
219
- // Check if it is one of the fallback servers
220
- const fallbackIdps = await this.getFallbackProviderParticipants();
221
- const fallbackServers = fallbackIdps.map(({ AuthorisationServers }) => AuthorisationServers).flat();
222
- found = fallbackServers.find(({ AuthorisationServerId }) => AuthorisationServerId === authServerId);
223
- if (!found) {
224
- // Let the user know if was an auth server that was filtered out
225
- const allAuthServers = (await this.retrieveFullParticipantsList(this.config.data.registry_participants_uri)).map(({ AuthorisationServers }) => AuthorisationServers).flat();
226
- let exists = allAuthServers.find(({ AuthorisationServerId }) => AuthorisationServerId === authServerId);
227
- let additionalLogInfo = exists ? ` Server exists but was filtered from results due to config settings for 'include_uncertified_participants', 'required_participant_certifications' and 'required_claims'` : '';
228
- throw new Error(`Unable to find specified Authorisation Server: ${authServerId}${additionalLogInfo}`);
229
- }
230
- }
231
- return found;
232
- }
233
- // Turn the list of claim names into the claims request to use as part of the PAR
234
- generateClaimsRequest(essentialClaims, voluntaryClaims) {
235
- this.logger.info(`Generating claims request with - essential claims: ${essentialClaims}, voluntary claims: ${voluntaryClaims}`);
236
- const formattedVoluntaryClaims = voluntaryClaims.filter((claim) => !essentialClaims.includes(claim));
237
- // Find the essential and voluntary claims.
238
- const split = (claim) => extendedClaimList.includes(claim);
239
- const [essentialExtendedClaims, filteredEssentialClaims] = partition(essentialClaims, split);
240
- const [voluntaryExtendedClaims, filteredFormattedVoluntaryClaims] = partition(formattedVoluntaryClaims, split);
241
- const buildExtendedClaims = (essentialExtendedClaims, voluntaryExtendedClaims) => [...essentialExtendedClaims, ...voluntaryExtendedClaims].reduce((acc, claim) => {
242
- return {
243
- ...acc,
244
- [claim]: {
245
- essential: essentialExtendedClaims.includes(claim),
246
- },
247
- };
248
- }, {});
249
- // If there are any extended claims, add them to the claims request.
250
- const claimsRequest = { id_token: {} };
251
- if (essentialExtendedClaims.length > 0 || voluntaryExtendedClaims.length > 0) {
252
- claimsRequest.id_token.verified_claims = {
253
- verification: {
254
- trust_framework: {
255
- value: 'au_connectid',
256
- },
257
- },
258
- claims: buildExtendedClaims(essentialExtendedClaims, voluntaryExtendedClaims),
259
- };
260
- }
261
- // Add the normal claims to the claims request.
262
- filteredEssentialClaims.forEach((claim) => (claimsRequest.id_token[claim] = { essential: true }));
263
- filteredFormattedVoluntaryClaims.forEach((claim) => (claimsRequest.id_token[claim] = { essential: false }));
264
- this.logger.info(`Claims request: ${JSON.stringify(claimsRequest)}`);
265
- return claimsRequest;
266
- }
267
- // Call the UserInfo endpoint to get the claims for the user
268
- // TODO type return?!
119
+ /**
120
+ * Retrieves tokens using an authorisation code.
121
+ *
122
+ * @param authorisationServerId - Authorisation server ID
123
+ * @param requestParams - OAuth callback parameters
124
+ * @param codeVerifier - PKCE code verifier from PAR
125
+ * @param state - State parameter from PAR
126
+ * @param nonce - Nonce parameter from PAR
127
+ * @returns Consolidated token set with validated claims
128
+ */
129
+ async retrieveTokens(authorisationServerId, requestParams, codeVerifier, state, nonce) {
130
+ return this.retrieveTokenEndpoint.retrieveTokens(authorisationServerId, requestParams, codeVerifier, state, nonce);
131
+ }
132
+ /**
133
+ * Retrieves user information from the UserInfo endpoint.
134
+ *
135
+ * @param authorisationServerId - Authorization server ID
136
+ * @param accessToken - Access token
137
+ * @returns UserInfo claims
138
+ */
269
139
  async getUserInfo(authorisationServerId, accessToken) {
270
- const authServer = await this.getAuthServerDetails(authorisationServerId);
271
- const { fapiClient } = await this.setupClient(authServer, this.generateXFapiInteractionId());
272
- return await fapiClient.userinfo(accessToken);
140
+ return this.userInfoEndpoint.getUserInfo(authorisationServerId, accessToken);
273
141
  }
274
- async getKeyset() {
275
- const key = createPrivateKey(this.signingKey);
276
- const privateJwk = await exportJWK(key);
277
- privateJwk.kid = this.config.data.signing_kid;
278
- this.logger.debug(`Create private jwk key ${JSON.stringify(privateJwk)}`);
279
- /*
280
- SDK bug with Node 16, the error is thrown in this function below because the JWK fails the !isPlainObject(k) condition.
281
-
282
- // rp-nodejs-sdk/node_modules/openid-client/lib/client.js
283
- function getKeystore(jwks) {
284
- if (
285
- !isPlainObject(jwks) ||
286
- !Array.isArray(jwks.keys) ||
287
- jwks.keys.some((k) => !isPlainObject(k) || !('kty' in k))
288
- ) {
289
- throw new TypeError('jwks must be a JSON Web Key Set formatted object');
290
- }
291
-
292
- return KeyStore.fromJWKS(jwks, { onlyPrivate: true });
293
- }
294
-
295
- Interesting that the code for isPlainObject is:
296
- module.exports = (a) => !!a && a.constructor === Object;
297
-
298
- And it returns false for a.constructor === Object. It returns [Function: Object]. So going deeper, I think the private key generated by const privateJwk = await fromKeyLike(key) is not compatible with the constructor even having the same type (JWK) as TS doesn't complain:
299
- new (metadata: ClientMetadata, jwks?: { keys: jose.JWK[] }, options?: ClientOptions): TClient;
300
-
301
- Might be different versions of JWK type (openid-client jose x jose)?!
302
- To make it work then I tried recreating the JWK object using JSON.stringify and JSON.parse so the constructor returns an Object (?!)
303
- ...and it worked :) So my guess is the way the JWK is generated using fromKeyLike that doesn't match the requirement when validating the JWK (constructor is not Object).
304
-
305
- Why does it work on Node 14?
306
- jwks.keys[0].constructor === Object on Node 14 returns true
307
- jwks.keys[0].constructor === Object on Node 16 returns false
308
- */
309
- return { keys: [JSON.parse(JSON.stringify(privateJwk))] };
310
- }
311
- // Create a FAPI client for the specified authorisation server
312
- async setupClient(authorisationServer, xFapiInteractionId) {
313
- const response = await fetch(authorisationServer.OpenIDDiscoveryDocument);
314
- if (!response.ok) {
315
- throw new Error(`Failed to retrieve metadata from the authorisation server ${authorisationServer.AuthorisationServerId} at ${authorisationServer.OpenIDDiscoveryDocument}`);
316
- }
317
- const metadata = await response.json();
318
- this.logger.debug(`Retrieved metadata for server ${authorisationServer.OpenIDDiscoveryDocument} ${JSON.stringify(metadata)}`);
319
- for (const [key, value] of Object.entries(metadata?.mtls_endpoint_aliases)) {
320
- metadata[key] = value;
321
- }
322
- this.logger.debug(`Updated endpoints with mtls alias properties for server ${authorisationServer.OpenIDDiscoveryDocument} ${JSON.stringify(metadata)}`);
323
- const localIssuer = new Issuer(metadata);
324
- this.logger.debug(`Discovered issuer ${localIssuer.issuer} ${JSON.stringify(localIssuer.metadata)}`);
325
- const keyset = await this.getKeyset();
326
- const fapiClient = new localIssuer.FAPI1Client(this.config.data.client, keyset);
327
- this.logger.debug(`Discovered client ${JSON.stringify(fapiClient)}`);
328
- fapiClient[custom.http_options] = () => ({
329
- key: this.transportKey,
330
- cert: this.transportPem,
331
- headers: { 'x-fapi-interaction-id': xFapiInteractionId },
332
- });
333
- return { fapiClient, localIssuer };
334
- }
335
- async generateRequest(fapiClient, claims, purpose) {
336
- const { codeChallenge, codeVerifier, nonce, state } = generatePushAuthorisationRequestParams();
337
- const request = await fapiClient.requestObject({
338
- scope: 'openid',
339
- response_type: 'code',
340
- redirect_uri: this.config.data.application_redirect_uri,
341
- code_challenge: codeChallenge,
342
- code_challenge_method: 'S256',
343
- response_mode: 'query',
344
- state,
345
- nonce,
346
- claims,
347
- prompt: 'consent',
348
- max_age: 900,
349
- purpose,
350
- });
351
- const clientAssertionPayload = {
352
- clientAssertionPayload: {
353
- aud: fapiClient.issuer.issuer,
354
- },
355
- };
356
- this.logger.debug('Generated request object: ' + JSON.stringify(request));
357
- const { request_uri } = await fapiClient.pushedAuthorizationRequest({ request }, clientAssertionPayload);
358
- const authUrl = fapiClient.authorizationUrl({ request_uri, scope: undefined, redirect_uri: undefined, response_type: undefined });
359
- return { authUrl, code_verifier: codeVerifier, state, nonce };
360
- }
361
- generateXFapiInteractionId() {
362
- return randomUUID();
142
+ /**
143
+ * Gets the current date (for testing purposes).
144
+ *
145
+ * @returns Current date
146
+ */
147
+ getCurrentDate() {
148
+ return new Date();
363
149
  }
364
150
  }