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

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 (97) hide show
  1. package/README.md +60 -71
  2. package/package.json +4 -5
  3. package/{config.js → src/config.js} +2 -31
  4. package/src/conformance/api/conformance-api.d.ts +38 -0
  5. package/src/conformance/api/conformance-api.js +53 -0
  6. package/src/conformance/config.json +60 -0
  7. package/src/conformance/conformance-config.d.ts +2 -0
  8. package/src/conformance/conformance-config.js +34 -0
  9. package/src/conformance/conformance.test.js +101 -0
  10. package/src/conformance/variant.json +1 -0
  11. package/src/crypto/crypto-loader.d.ts +32 -0
  12. package/src/crypto/crypto-loader.js +49 -0
  13. package/src/crypto/jwt-helper.d.ts +61 -0
  14. package/src/crypto/jwt-helper.js +92 -0
  15. package/src/crypto/pkce-helper.d.ts +43 -0
  16. package/src/crypto/pkce-helper.js +75 -0
  17. package/src/endpoints/participants-endpoint.d.ts +55 -0
  18. package/src/endpoints/participants-endpoint.js +137 -0
  19. package/src/endpoints/pushed-authorisation-request-endpoint.d.ts +87 -0
  20. package/src/endpoints/pushed-authorisation-request-endpoint.js +192 -0
  21. package/src/endpoints/retrieve-token-endpoint.d.ts +66 -0
  22. package/src/endpoints/retrieve-token-endpoint.js +159 -0
  23. package/src/endpoints/userinfo-endpoint.d.ts +24 -0
  24. package/src/endpoints/userinfo-endpoint.js +50 -0
  25. package/src/fapi/fapi-utils.d.ts +6 -0
  26. package/src/fapi/fapi-utils.js +9 -0
  27. package/src/http/http-client-extensions.d.ts +60 -0
  28. package/src/http/http-client-extensions.js +106 -0
  29. package/src/http/http-client-factory.d.ts +27 -0
  30. package/src/http/http-client-factory.js +45 -0
  31. package/src/integration/integration.test.d.ts +1 -0
  32. package/src/integration/integration.test.js +30 -0
  33. package/src/model/callback-params.d.ts +31 -0
  34. package/src/model/callback-params.js +1 -0
  35. package/src/model/claims.d.ts +100 -0
  36. package/src/model/claims.js +1 -0
  37. package/src/model/consolidated-token-set.d.ts +74 -0
  38. package/src/model/consolidated-token-set.js +100 -0
  39. package/src/model/discovery-service.d.ts +46 -0
  40. package/src/model/discovery-service.js +112 -0
  41. package/src/model/issuer-metadata.d.ts +165 -0
  42. package/src/model/issuer-metadata.js +1 -0
  43. package/src/model/jwks.d.ts +12 -0
  44. package/src/model/jwks.js +1 -0
  45. package/src/model/token-response.d.ts +31 -0
  46. package/src/model/token-response.js +1 -0
  47. package/src/model/token-set.d.ts +73 -0
  48. package/src/model/token-set.js +179 -0
  49. package/src/relying-party-client-sdk.d.ts +68 -0
  50. package/src/relying-party-client-sdk.js +150 -0
  51. package/src/test-data/large-participants-test-data.d.ts +865 -0
  52. package/src/test-data/large-participants-test-data.js +18907 -0
  53. package/src/test-data/participants-test-data.d.ts +149 -0
  54. package/src/test-data/participants-test-data.js +458 -0
  55. package/src/test-data/sandbox-participants-test-data.d.ts +865 -0
  56. package/src/test-data/sandbox-participants-test-data.js +3794 -0
  57. package/src/tests/cert-utils.test.d.ts +1 -0
  58. package/src/tests/cert-utils.test.js +13 -0
  59. package/src/tests/functional-utils.test.d.ts +1 -0
  60. package/src/tests/functional-utils.test.js +13 -0
  61. package/src/tests/participant-filters.test.d.ts +1 -0
  62. package/src/tests/participant-filters.test.js +151 -0
  63. package/src/tests/pushed-authorisation-request-endpoint.test.d.ts +1 -0
  64. package/src/tests/pushed-authorisation-request-endpoint.test.js +159 -0
  65. package/src/tests/relying-party-client-sdk.test.d.ts +1 -0
  66. package/src/tests/relying-party-client-sdk.test.js +313 -0
  67. package/src/tests/request-utils.test.d.ts +1 -0
  68. package/src/tests/request-utils.test.js +16 -0
  69. package/src/tests/system-information.test.d.ts +1 -0
  70. package/src/tests/system-information.test.js +16 -0
  71. package/src/tests/user-agent.test.d.ts +1 -0
  72. package/src/tests/user-agent.test.js +23 -0
  73. package/src/tests/validator.test.d.ts +1 -0
  74. package/src/tests/validator.test.js +38 -0
  75. package/{types.d.ts → src/types.d.ts} +61 -32
  76. package/src/types.js +1 -0
  77. package/{utils → src/utils}/request-utils.d.ts +1 -1
  78. package/src/utils/request-utils.js +8 -0
  79. package/{utils → src/utils}/user-agent.d.ts +1 -1
  80. package/{utils → src/utils}/user-agent.js +1 -1
  81. package/relying-party-client-sdk.d.ts +0 -37
  82. package/relying-party-client-sdk.js +0 -364
  83. package/utils/request-utils.js +0 -8
  84. /package/{config.d.ts → src/config.d.ts} +0 -0
  85. /package/{types.js → src/conformance/conformance.test.d.ts} +0 -0
  86. /package/{filter → src/filter}/participant-filters.d.ts +0 -0
  87. /package/{filter → src/filter}/participant-filters.js +0 -0
  88. /package/{logger.d.ts → src/logger.d.ts} +0 -0
  89. /package/{logger.js → src/logger.js} +0 -0
  90. /package/{utils → src/utils}/cert-utils.d.ts +0 -0
  91. /package/{utils → src/utils}/cert-utils.js +0 -0
  92. /package/{utils → src/utils}/functional-utils.d.ts +0 -0
  93. /package/{utils → src/utils}/functional-utils.js +0 -0
  94. /package/{utils → src/utils}/system-information.d.ts +0 -0
  95. /package/{utils → src/utils}/system-information.js +0 -0
  96. /package/{validator.d.ts → src/validator.d.ts} +0 -0
  97. /package/{validator.js → src/validator.js} +0 -0
@@ -0,0 +1,87 @@
1
+ import { Agent } from 'undici';
2
+ import { Logger } from 'winston';
3
+ import { RelyingPartyClientSdkConfig } from '../types.js';
4
+ import { JwtHelper } from '../crypto/jwt-helper.js';
5
+ import { ParticipantsEndpoint } from './participants-endpoint';
6
+ /**
7
+ * Response from the Pushed Authorization Request endpoint.
8
+ */
9
+ export interface PARResponse {
10
+ /**
11
+ * The full authorization URL to redirect the user to.
12
+ */
13
+ authUrl: string;
14
+ /**
15
+ * The PKCE code verifier (to be sent with token request).
16
+ */
17
+ codeVerifier: string;
18
+ /**
19
+ * The state parameter (for CSRF protection).
20
+ */
21
+ state: string;
22
+ /**
23
+ * The nonce parameter (for ID token validation).
24
+ */
25
+ nonce: string;
26
+ /**
27
+ * The fapi-interaction-id
28
+ */
29
+ xFapiInteractionId: string;
30
+ }
31
+ /**
32
+ * Pushed Authorization Request Endpoint
33
+ *
34
+ * Handles the PAR flow as defined in RFC 9126.
35
+ * Creates signed request objects and submits them to the PAR endpoint.
36
+ */
37
+ export declare class PushedAuthorisationRequestEndpoint {
38
+ private readonly sdkConfig;
39
+ private readonly httpClient;
40
+ private readonly jwtHelper;
41
+ private readonly logger;
42
+ private readonly participantsEndpoint;
43
+ private static readonly EXTENDED_CLAIMS;
44
+ constructor(sdkConfig: RelyingPartyClientSdkConfig, httpClient: Agent, jwtHelper: JwtHelper, logger: Logger, participantsEndpoint: ParticipantsEndpoint);
45
+ /**
46
+ * Sends a Pushed Authorization Request to the authorization server.
47
+ *
48
+ * @param authServerId - Authorization server details
49
+ * @param essentialClaims - Claims that must be provided
50
+ * @param voluntaryClaims - Claims that are optional
51
+ * @param purpose - Purpose string for data sharing
52
+ * @returns PAR response with authorization URL and PKCE parameters
53
+ */
54
+ sendPushedAuthorisationRequest(authServerId: string, essentialClaims: string[], voluntaryClaims: string[], purpose: string): Promise<PARResponse>;
55
+ private generateRequest;
56
+ /**
57
+ * Generates the claims request object.
58
+ *
59
+ * Separates claims into basic and extended categories.
60
+ * Extended claims go into the verified_claims structure.
61
+ *
62
+ * @param essentialClaims - Claims that must be provided
63
+ * @param voluntaryClaims - Claims that are optional
64
+ * @returns Structured claims request
65
+ */
66
+ private generateClaimsRequest;
67
+ /**
68
+ * Sends the PAR request to the authorization server.
69
+ *
70
+ * @param parEndpoint - PAR endpoint URL
71
+ * @param requestJwt - Signed request JWT
72
+ * @param clientAssertion - Client assertion JWT for authentication
73
+ * @param xFapiInteractionId - FAPI interaction ID
74
+ * @returns PAR endpoint response
75
+ */
76
+ private sendPARRequest;
77
+ /**
78
+ * Constructs the authorization URL.
79
+ *
80
+ * Per RFC 9126, only client_id and request_uri should be included.
81
+ *
82
+ * @param authorizationEndpoint - Authorization endpoint URL
83
+ * @param requestUri - Request URI from PAR response
84
+ * @returns Full authorization URL
85
+ */
86
+ private constructAuthorizationUrl;
87
+ }
@@ -0,0 +1,192 @@
1
+ import { PkceHelper } from '../crypto/pkce-helper.js';
2
+ import { DiscoveryService } from '../model/discovery-service.js';
3
+ import { HttpClientExtensions } from '../http/http-client-extensions.js';
4
+ import { illegalPurposeChars, validatePurpose } from '../validator.js';
5
+ import { generateXFapiInteractionId } from '../fapi/fapi-utils.js';
6
+ /**
7
+ * Pushed Authorization Request Endpoint
8
+ *
9
+ * Handles the PAR flow as defined in RFC 9126.
10
+ * Creates signed request objects and submits them to the PAR endpoint.
11
+ */
12
+ export class PushedAuthorisationRequestEndpoint {
13
+ constructor(sdkConfig, httpClient, jwtHelper, logger, participantsEndpoint) {
14
+ this.sdkConfig = sdkConfig;
15
+ this.httpClient = httpClient;
16
+ this.jwtHelper = jwtHelper;
17
+ this.logger = logger;
18
+ this.participantsEndpoint = participantsEndpoint;
19
+ }
20
+ /**
21
+ * Sends a Pushed Authorization Request to the authorization server.
22
+ *
23
+ * @param authServerId - Authorization server details
24
+ * @param essentialClaims - Claims that must be provided
25
+ * @param voluntaryClaims - Claims that are optional
26
+ * @param purpose - Purpose string for data sharing
27
+ * @returns PAR response with authorization URL and PKCE parameters
28
+ */
29
+ async sendPushedAuthorisationRequest(authServerId, essentialClaims, voluntaryClaims, purpose) {
30
+ // Validate purpose
31
+ const purposeValidation = validatePurpose(purpose);
32
+ if (purposeValidation === 'INVALID_LENGTH') {
33
+ this.logger.warn('Purpose must be between 3 and 300 characters');
34
+ throw new Error(`Invalid purpose for supplied for PAR: ${purpose}`);
35
+ }
36
+ if (purposeValidation === 'INVALID_CHARACTERS') {
37
+ this.logger.warn(`Purpose cannot contain any of the following characters: ${illegalPurposeChars.join(',')}, purpose supplied: [${purpose}]`);
38
+ throw new Error(`Invalid purpose for supplied for PAR: ${purpose}`);
39
+ }
40
+ const xFapiInteractionId = generateXFapiInteractionId();
41
+ try {
42
+ const essentialClaimsWithTxn = [...new Set([...essentialClaims, 'txn'])];
43
+ const claimsRequest = this.generateClaimsRequest(essentialClaims, voluntaryClaims);
44
+ const authServer = await this.participantsEndpoint.getAuthServerDetails(authServerId);
45
+ const { authUrl, codeVerifier, state, nonce } = await this.generateRequest(authServer, claimsRequest, purpose, xFapiInteractionId);
46
+ this.logger.info(`Sent PAR to auth server: ${authServer.AuthorisationServerId} - ${authServer.CustomerFriendlyName}, essential claims: ${essentialClaimsWithTxn}, voluntary claims: ${voluntaryClaims}, x-fapi-interaction-id: ${xFapiInteractionId}`);
47
+ return {
48
+ authUrl,
49
+ codeVerifier: codeVerifier,
50
+ state,
51
+ nonce,
52
+ xFapiInteractionId,
53
+ };
54
+ }
55
+ catch (err) {
56
+ this.logger.error(`Error sending pushed authorisation request to authorisation server ${authServerId}, x-fapi-interaction-id: ${xFapiInteractionId}.`, err);
57
+ throw new Error(`Unable to send pushed authorisation request to ${authServerId}, x-fapi-interaction-id: ${xFapiInteractionId} - ${err}`);
58
+ }
59
+ }
60
+ async generateRequest(authServer, claimsRequest, purpose, xFapiInteractionId) {
61
+ // Fetch discovery document
62
+ const discoveryMetadata = await DiscoveryService.fetchDiscoveryDocument(authServer.OpenIDDiscoveryDocument, this.httpClient);
63
+ if (!discoveryMetadata.pushed_authorization_request_endpoint) {
64
+ throw new Error(`Authorization server ${authServer.AuthorisationServerId} does not support PAR`);
65
+ }
66
+ // Generate PKCE parameters
67
+ const state = PkceHelper.generateState();
68
+ const nonce = PkceHelper.generateNonce();
69
+ const codeVerifier = PkceHelper.generateCodeVerifier();
70
+ const codeChallenge = PkceHelper.generateCodeChallenge(codeVerifier);
71
+ this.logger.debug(`Generated PKCE parameters - state: ${state.substring(0, 10)}..., nonce: ${nonce.substring(0, 10)}...`);
72
+ this.logger.debug(`Claims request: ${JSON.stringify(claimsRequest, null, 2)}`);
73
+ // Create signed request JWT
74
+ const requestJwt = await this.jwtHelper.generateRequestJwt({
75
+ issuer: this.sdkConfig.data.client_id,
76
+ audience: discoveryMetadata.issuer,
77
+ redirectUri: this.sdkConfig.data.application_redirect_uri,
78
+ scope: 'openid',
79
+ responseType: 'code',
80
+ codeChallenge,
81
+ codeChallengeMethod: 'S256',
82
+ state,
83
+ nonce,
84
+ claims: claimsRequest,
85
+ purpose,
86
+ prompt: 'consent',
87
+ });
88
+ // Create client assertion for authentication
89
+ const clientAssertion = await this.jwtHelper.generateClientAssertionJwt(discoveryMetadata.issuer);
90
+ // Send PAR request
91
+ const parResponse = await this.sendPARRequest(discoveryMetadata.pushed_authorization_request_endpoint, requestJwt, clientAssertion, xFapiInteractionId);
92
+ this.logger.info(`PAR request successful, request_uri: ${parResponse.request_uri}, expires_in: ${parResponse.expires_in}s`);
93
+ // Construct authorization URL
94
+ // RFC 9126: Only client_id and request_uri should be sent
95
+ const authUrl = this.constructAuthorizationUrl(discoveryMetadata.authorization_endpoint, parResponse.request_uri);
96
+ this.logger.debug(`Authorization URL: ${authUrl}`);
97
+ return {
98
+ authUrl,
99
+ codeVerifier: codeVerifier,
100
+ state,
101
+ nonce,
102
+ };
103
+ }
104
+ /**
105
+ * Generates the claims request object.
106
+ *
107
+ * Separates claims into basic and extended categories.
108
+ * Extended claims go into the verified_claims structure.
109
+ *
110
+ * @param essentialClaims - Claims that must be provided
111
+ * @param voluntaryClaims - Claims that are optional
112
+ * @returns Structured claims request
113
+ */
114
+ generateClaimsRequest(essentialClaims, voluntaryClaims) {
115
+ // Remove duplicates (essential takes precedence)
116
+ const filteredVoluntary = voluntaryClaims.filter((c) => !essentialClaims.includes(c));
117
+ // Partition claims into basic and extended
118
+ const essentialExtended = essentialClaims.filter((c) => PushedAuthorisationRequestEndpoint.EXTENDED_CLAIMS.includes(c));
119
+ const essentialBasic = essentialClaims.filter((c) => !PushedAuthorisationRequestEndpoint.EXTENDED_CLAIMS.includes(c));
120
+ const voluntaryExtended = filteredVoluntary.filter((c) => PushedAuthorisationRequestEndpoint.EXTENDED_CLAIMS.includes(c));
121
+ const voluntaryBasic = filteredVoluntary.filter((c) => !PushedAuthorisationRequestEndpoint.EXTENDED_CLAIMS.includes(c));
122
+ const claimsRequest = { id_token: {} };
123
+ // Add basic claims directly to id_token
124
+ essentialBasic.forEach((claim) => {
125
+ claimsRequest.id_token[claim] = { essential: true };
126
+ });
127
+ voluntaryBasic.forEach((claim) => {
128
+ claimsRequest.id_token[claim] = { essential: false };
129
+ });
130
+ // Add extended claims to verified_claims structure
131
+ if (essentialExtended.length > 0 || voluntaryExtended.length > 0) {
132
+ claimsRequest.id_token.verified_claims = {
133
+ verification: {
134
+ trust_framework: { value: 'au_connectid' },
135
+ },
136
+ claims: {},
137
+ };
138
+ essentialExtended.forEach((claim) => {
139
+ claimsRequest.id_token.verified_claims.claims[claim] = {
140
+ essential: true,
141
+ };
142
+ });
143
+ voluntaryExtended.forEach((claim) => {
144
+ claimsRequest.id_token.verified_claims.claims[claim] = {
145
+ essential: false,
146
+ };
147
+ });
148
+ }
149
+ return claimsRequest;
150
+ }
151
+ /**
152
+ * Sends the PAR request to the authorization server.
153
+ *
154
+ * @param parEndpoint - PAR endpoint URL
155
+ * @param requestJwt - Signed request JWT
156
+ * @param clientAssertion - Client assertion JWT for authentication
157
+ * @param xFapiInteractionId - FAPI interaction ID
158
+ * @returns PAR endpoint response
159
+ */
160
+ async sendPARRequest(parEndpoint, requestJwt, clientAssertion, xFapiInteractionId) {
161
+ const body = new URLSearchParams({
162
+ request: requestJwt,
163
+ client_assertion: clientAssertion,
164
+ client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
165
+ });
166
+ this.logger.debug(`Sending PAR request to: ${parEndpoint}`);
167
+ this.logger.debug(` with body: ${body}`);
168
+ const response = await HttpClientExtensions.postForm(parEndpoint, body, {
169
+ agent: this.httpClient,
170
+ clientId: this.sdkConfig.data.client_id,
171
+ xFapiInteractionId,
172
+ });
173
+ return HttpClientExtensions.parseJsonResponse(response);
174
+ }
175
+ /**
176
+ * Constructs the authorization URL.
177
+ *
178
+ * Per RFC 9126, only client_id and request_uri should be included.
179
+ *
180
+ * @param authorizationEndpoint - Authorization endpoint URL
181
+ * @param requestUri - Request URI from PAR response
182
+ * @returns Full authorization URL
183
+ */
184
+ constructAuthorizationUrl(authorizationEndpoint, requestUri) {
185
+ const url = new URL(authorizationEndpoint);
186
+ url.searchParams.set('client_id', this.sdkConfig.data.client_id);
187
+ url.searchParams.set('request_uri', requestUri);
188
+ return url.toString();
189
+ }
190
+ }
191
+ // Extended claims list for ConnectID
192
+ PushedAuthorisationRequestEndpoint.EXTENDED_CLAIMS = ['over16', 'over18', 'over21', 'over25', 'over65', 'beneficiary_account_au', 'beneficiary_account_au_payid', 'beneficiary_account_international', 'cba_loyalty'];
@@ -0,0 +1,66 @@
1
+ import { Agent } from 'undici';
2
+ import { Logger } from 'winston';
3
+ import { CallbackParams, RelyingPartyClientSdkConfig } from '../types.js';
4
+ import { JwtHelper } from '../crypto/jwt-helper.js';
5
+ import { ConsolidatedTokenSet } from '../model/consolidated-token-set.js';
6
+ import { ParticipantsEndpoint } from './participants-endpoint';
7
+ /**
8
+ * Retrieve Token Endpoint
9
+ *
10
+ * Handles the token exchange flow (authorization code for tokens).
11
+ * Validates the ID token and optionally calls UserInfo for compliance.
12
+ */
13
+ export declare class RetrieveTokenEndpoint {
14
+ private readonly sdkConfig;
15
+ private readonly httpClient;
16
+ private readonly jwtHelper;
17
+ private readonly logger;
18
+ private readonly participantsEndpoint;
19
+ constructor(sdkConfig: RelyingPartyClientSdkConfig, httpClient: Agent, jwtHelper: JwtHelper, logger: Logger, participantsEndpoint: ParticipantsEndpoint);
20
+ /**
21
+ * Retrieves tokens from the authorization server.
22
+ *
23
+ * Performs the following steps:
24
+ * 1. Validates callback parameters
25
+ * 2. Exchanges authorization code for tokens
26
+ * 3. Validates ID token
27
+ * 4. Optionally calls UserInfo for compliance
28
+ *
29
+ * @param authorisationServerId - Authorization server id
30
+ * @param callbackParams - OAuth callback parameters from redirect
31
+ * @param codeVerifier - PKCE code verifier from PAR
32
+ * @param state - State parameter from PAR
33
+ * @param nonce - Nonce parameter from PAR
34
+ * @returns Consolidated token set with validated claims
35
+ */
36
+ retrieveTokens(authorisationServerId: string, callbackParams: CallbackParams, codeVerifier: string, state: string, nonce: string): Promise<ConsolidatedTokenSet>;
37
+ /**
38
+ * Validates the OAuth callback parameters.
39
+ *
40
+ * @param callbackParams - Callback parameters from redirect
41
+ * @param expectedState - Expected state value
42
+ * @throws Error if validation fails
43
+ */
44
+ private validateCallbackParams;
45
+ /**
46
+ * Requests tokens from the token endpoint.
47
+ *
48
+ * @param tokenEndpoint - Token endpoint URL
49
+ * @param code - Authorization code from callback
50
+ * @param codeVerifier - PKCE code verifier
51
+ * @param clientAssertion - Client assertion JWT for authentication
52
+ * @param xFapiInteractionId - FAPI interaction ID
53
+ * @returns Token response
54
+ */
55
+ private requestToken;
56
+ /**
57
+ * Calls the UserInfo endpoint for compliance verification.
58
+ *
59
+ * This is required by some conformance test suites.
60
+ *
61
+ * @param userinfoEndpoint - UserInfo endpoint URL
62
+ * @param accessToken - Access token
63
+ * @param xFapiInteractionId - FAPI interaction ID
64
+ */
65
+ private callUserInfoForCompliance;
66
+ }
@@ -0,0 +1,159 @@
1
+ import { DiscoveryService } from '../model/discovery-service.js';
2
+ import { HttpClientExtensions } from '../http/http-client-extensions.js';
3
+ import { TokenSet } from '../model/token-set.js';
4
+ import { ConsolidatedTokenSet } from '../model/consolidated-token-set.js';
5
+ import { generateXFapiInteractionId } from '../fapi/fapi-utils.js';
6
+ /**
7
+ * Retrieve Token Endpoint
8
+ *
9
+ * Handles the token exchange flow (authorization code for tokens).
10
+ * Validates the ID token and optionally calls UserInfo for compliance.
11
+ */
12
+ export class RetrieveTokenEndpoint {
13
+ constructor(sdkConfig, httpClient, jwtHelper, logger, participantsEndpoint) {
14
+ this.sdkConfig = sdkConfig;
15
+ this.httpClient = httpClient;
16
+ this.jwtHelper = jwtHelper;
17
+ this.logger = logger;
18
+ this.participantsEndpoint = participantsEndpoint;
19
+ }
20
+ /**
21
+ * Retrieves tokens from the authorization server.
22
+ *
23
+ * Performs the following steps:
24
+ * 1. Validates callback parameters
25
+ * 2. Exchanges authorization code for tokens
26
+ * 3. Validates ID token
27
+ * 4. Optionally calls UserInfo for compliance
28
+ *
29
+ * @param authorisationServerId - Authorization server id
30
+ * @param callbackParams - OAuth callback parameters from redirect
31
+ * @param codeVerifier - PKCE code verifier from PAR
32
+ * @param state - State parameter from PAR
33
+ * @param nonce - Nonce parameter from PAR
34
+ * @returns Consolidated token set with validated claims
35
+ */
36
+ async retrieveTokens(authorisationServerId, callbackParams, codeVerifier, state, nonce) {
37
+ const xFapiInteractionId = generateXFapiInteractionId();
38
+ try {
39
+ this.logger.info(`Retrieving tokens for auth server: ${authorisationServerId}`);
40
+ // Validate callback parameters
41
+ this.validateCallbackParams(callbackParams, state);
42
+ const authServer = await this.participantsEndpoint.getAuthServerDetails(authorisationServerId);
43
+ // Fetch discovery document
44
+ const discoveryMetadata = await DiscoveryService.fetchDiscoveryDocument(authServer.OpenIDDiscoveryDocument, this.httpClient);
45
+ // Validate issuer parameter (REQUIRED per FAPI 2.0)
46
+ if (!callbackParams.iss) {
47
+ throw new Error('Authorization response missing required iss parameter');
48
+ }
49
+ if (callbackParams.iss !== discoveryMetadata.issuer) {
50
+ throw new Error(`Issuer mismatch: expected ${discoveryMetadata.issuer}, got ${callbackParams.iss}`);
51
+ }
52
+ // Create client assertion for authentication
53
+ const clientAssertion = await this.jwtHelper.generateClientAssertionJwt(discoveryMetadata.issuer);
54
+ // Exchange authorization code for tokens
55
+ const tokenResponse = await this.requestToken(discoveryMetadata.token_endpoint, callbackParams.code, codeVerifier, clientAssertion, xFapiInteractionId);
56
+ const tokenSet = new TokenSet(tokenResponse);
57
+ const jwks = await DiscoveryService.fetchJwks(discoveryMetadata.jwks_uri, this.httpClient);
58
+ await tokenSet.validate(jwks, discoveryMetadata.issuer, this.sdkConfig.data.client_id, nonce, discoveryMetadata.id_token_signing_alg_values_supported);
59
+ // Must call validate first before accessing claims
60
+ this.logger.info(`Retrieved tokenSet from auth server: ${authorisationServerId} - ${authServer.CustomerFriendlyName}, x-fapi-interaction-id: ${xFapiInteractionId}, txn: ${tokenSet.claims().txn}`);
61
+ this.logger.debug(`Tokens returned from: ${authorisationServerId} - ${authServer.CustomerFriendlyName}: ${JSON.stringify(tokenSet)} claims: ${JSON.stringify(tokenSet.claims())}`);
62
+ const consolidatedTokenSet = new ConsolidatedTokenSet(tokenSet, xFapiInteractionId);
63
+ // If using the OIDF test suite, need to make a call to userInfo when claims were successfully decoded
64
+ if (this.sdkConfig.data.enable_auto_compliance_verification) {
65
+ this.logger.warn('Auto compliance verification mode is enabled - calling UserInfo endpoint. This mode should only be enabled while performing OIDF conformance suite testing and will cause issues if used in production.');
66
+ if (!tokenSet.access_token) {
67
+ throw new Error('Cannot call UserInfo: no access token present');
68
+ }
69
+ if (!discoveryMetadata.userinfo_endpoint) {
70
+ throw new Error('Authorization server does not have a UserInfo endpoint');
71
+ }
72
+ // Call UserInfo endpoint for compliance
73
+ await this.callUserInfoForCompliance(discoveryMetadata.userinfo_endpoint, tokenSet.access_token, xFapiInteractionId);
74
+ }
75
+ return consolidatedTokenSet;
76
+ }
77
+ catch (err) {
78
+ this.logger.error(`Error retrieving tokens with authorisation server ${authorisationServerId}, x-fapi-interaction-id: ${xFapiInteractionId}`, err);
79
+ throw new Error(`Unable to retrieve tokens with authorisation server ${authorisationServerId}, x-fapi-interaction-id: ${xFapiInteractionId}`);
80
+ }
81
+ }
82
+ /**
83
+ * Validates the OAuth callback parameters.
84
+ *
85
+ * @param callbackParams - Callback parameters from redirect
86
+ * @param expectedState - Expected state value
87
+ * @throws Error if validation fails
88
+ */
89
+ validateCallbackParams(callbackParams, expectedState) {
90
+ // Check for error response
91
+ if (callbackParams.error) {
92
+ throw new Error(`Authorization failed: ${callbackParams.error}${callbackParams.error_description ? ` - ${callbackParams.error_description}` : ''}`);
93
+ }
94
+ // Validate code is present
95
+ if (!callbackParams.code) {
96
+ throw new Error('Authorization code missing from callback parameters');
97
+ }
98
+ // Validate state
99
+ if (!callbackParams.state) {
100
+ throw new Error('State parameter missing from callback parameters');
101
+ }
102
+ if (callbackParams.state !== expectedState) {
103
+ throw new Error(`State mismatch: expected ${expectedState}, got ${callbackParams.state}`);
104
+ }
105
+ }
106
+ /**
107
+ * Requests tokens from the token endpoint.
108
+ *
109
+ * @param tokenEndpoint - Token endpoint URL
110
+ * @param code - Authorization code from callback
111
+ * @param codeVerifier - PKCE code verifier
112
+ * @param clientAssertion - Client assertion JWT for authentication
113
+ * @param xFapiInteractionId - FAPI interaction ID
114
+ * @returns Token response
115
+ */
116
+ async requestToken(tokenEndpoint, code, codeVerifier, clientAssertion, xFapiInteractionId) {
117
+ const body = new URLSearchParams({
118
+ grant_type: 'authorization_code',
119
+ code,
120
+ redirect_uri: this.sdkConfig.data.application_redirect_uri,
121
+ code_verifier: codeVerifier,
122
+ client_assertion: clientAssertion,
123
+ client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
124
+ });
125
+ this.logger.debug(`Requesting tokens from: ${tokenEndpoint}`);
126
+ this.logger.debug(` with body: ${body}`);
127
+ const response = await HttpClientExtensions.postForm(tokenEndpoint, body, {
128
+ agent: this.httpClient,
129
+ clientId: this.sdkConfig.data.client_id,
130
+ xFapiInteractionId,
131
+ });
132
+ return HttpClientExtensions.parseJsonResponse(response);
133
+ }
134
+ /**
135
+ * Calls the UserInfo endpoint for compliance verification.
136
+ *
137
+ * This is required by some conformance test suites.
138
+ *
139
+ * @param userinfoEndpoint - UserInfo endpoint URL
140
+ * @param accessToken - Access token
141
+ * @param xFapiInteractionId - FAPI interaction ID
142
+ */
143
+ async callUserInfoForCompliance(userinfoEndpoint, accessToken, xFapiInteractionId) {
144
+ try {
145
+ const response = await HttpClientExtensions.getWithToken(userinfoEndpoint, accessToken, {
146
+ agent: this.httpClient,
147
+ clientId: this.sdkConfig.data.client_id,
148
+ xFapiInteractionId,
149
+ });
150
+ const userInfo = await HttpClientExtensions.parseJsonResponse(response);
151
+ this.logger.debug(`UserInfo response: ${JSON.stringify(userInfo, null, 2)}`);
152
+ this.logger.info('UserInfo endpoint called successfully for compliance: ' + userinfoEndpoint);
153
+ }
154
+ catch (error) {
155
+ this.logger.warn(`UserInfo call failed (compliance verification): ${error instanceof Error ? error.message : String(error)}`);
156
+ // Don't throw - this is just for compliance, not critical
157
+ }
158
+ }
159
+ }
@@ -0,0 +1,24 @@
1
+ import { Agent } from 'undici';
2
+ import { Logger } from 'winston';
3
+ import { ParticipantsEndpoint } from './participants-endpoint';
4
+ /**
5
+ * UserInfo Endpoint
6
+ *
7
+ * Handles calls to the OIDC UserInfo endpoint.
8
+ * Returns user claims using an access token.
9
+ */
10
+ export declare class UserInfoEndpoint {
11
+ private readonly httpClient;
12
+ private readonly logger;
13
+ private readonly clientId;
14
+ private readonly participantsEndpoint;
15
+ constructor(httpClient: Agent, logger: Logger, clientId: string, participantsEndpoint: ParticipantsEndpoint);
16
+ /**
17
+ * Retrieves user information from the UserInfo endpoint.
18
+ *
19
+ * @param authorisationServerId - Authorization server id
20
+ * @param accessToken - Access token to authenticate the request
21
+ * @returns UserInfo claims
22
+ */
23
+ getUserInfo(authorisationServerId: string, accessToken: string): Promise<Record<string, unknown>>;
24
+ }
@@ -0,0 +1,50 @@
1
+ import { DiscoveryService } from '../model/discovery-service.js';
2
+ import { HttpClientExtensions } from '../http/http-client-extensions.js';
3
+ import { generateXFapiInteractionId } from '../fapi/fapi-utils.js';
4
+ /**
5
+ * UserInfo Endpoint
6
+ *
7
+ * Handles calls to the OIDC UserInfo endpoint.
8
+ * Returns user claims using an access token.
9
+ */
10
+ export class UserInfoEndpoint {
11
+ constructor(httpClient, logger, clientId, participantsEndpoint) {
12
+ this.httpClient = httpClient;
13
+ this.logger = logger;
14
+ this.clientId = clientId;
15
+ this.participantsEndpoint = participantsEndpoint;
16
+ }
17
+ /**
18
+ * Retrieves user information from the UserInfo endpoint.
19
+ *
20
+ * @param authorisationServerId - Authorization server id
21
+ * @param accessToken - Access token to authenticate the request
22
+ * @returns UserInfo claims
23
+ */
24
+ async getUserInfo(authorisationServerId, accessToken) {
25
+ this.logger.info(`Fetching UserInfo for auth server: ${authorisationServerId}`);
26
+ const xFapiInteractionId = generateXFapiInteractionId();
27
+ try {
28
+ const authServer = await this.participantsEndpoint.getAuthServerDetails(authorisationServerId);
29
+ // Fetch discovery document
30
+ const discoveryMetadata = await DiscoveryService.fetchDiscoveryDocument(authServer.OpenIDDiscoveryDocument, this.httpClient);
31
+ if (!discoveryMetadata.userinfo_endpoint) {
32
+ throw new Error(`Authorization server ${authServer.AuthorisationServerId} does not have a UserInfo endpoint`);
33
+ }
34
+ // Call UserInfo endpoint
35
+ const response = await HttpClientExtensions.getWithToken(discoveryMetadata.userinfo_endpoint, accessToken, {
36
+ agent: this.httpClient,
37
+ clientId: this.clientId,
38
+ xFapiInteractionId,
39
+ });
40
+ const userInfo = await HttpClientExtensions.parseJsonResponse(response);
41
+ this.logger.info(`UserInfo retrieved successfully, x-fapi-interaction-id: ${xFapiInteractionId}`);
42
+ this.logger.debug(`UserInfo claims: ${JSON.stringify(userInfo, null, 2)}`);
43
+ return userInfo;
44
+ }
45
+ catch (err) {
46
+ this.logger.error(`Error calling user info authorisation server ${authorisationServerId}, x-fapi-interaction-id: ${xFapiInteractionId}`, err);
47
+ throw new Error(`Unable to calling user info with authorisation server ${authorisationServerId}, x-fapi-interaction-id: ${xFapiInteractionId}`);
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Generates a unique x-fapi-interaction-id.
3
+ *
4
+ * @returns UUID string
5
+ */
6
+ export declare const generateXFapiInteractionId: () => `${string}-${string}-${string}-${string}-${string}`;
@@ -0,0 +1,9 @@
1
+ import { randomUUID } from 'crypto';
2
+ /**
3
+ * Generates a unique x-fapi-interaction-id.
4
+ *
5
+ * @returns UUID string
6
+ */
7
+ export const generateXFapiInteractionId = () => {
8
+ return randomUUID();
9
+ };
@@ -0,0 +1,60 @@
1
+ import { Agent, Dispatcher } from 'undici';
2
+ export interface UndiciRequestInit extends RequestInit {
3
+ dispatcher?: Dispatcher;
4
+ }
5
+ export interface FetchOptions extends UndiciRequestInit {
6
+ agent?: Agent;
7
+ }
8
+ export interface FapiRequestOptions {
9
+ agent: Agent;
10
+ clientId: string;
11
+ xFapiInteractionId: string;
12
+ contentType?: string;
13
+ additionalHeaders?: Record<string, string>;
14
+ }
15
+ /**
16
+ * Utility functions for making HTTP requests with the configured mTLS client.
17
+ */
18
+ export declare class HttpClientExtensions {
19
+ /**
20
+ * Creates standard headers for FAPI requests.
21
+ *
22
+ * @param options - Request configuration including client ID and interaction ID
23
+ * @returns Headers object with required FAPI headers
24
+ */
25
+ static createFapiHeaders(options: FapiRequestOptions): HeadersInit;
26
+ /**
27
+ * Makes a POST request with form-encoded body.
28
+ *
29
+ * @param url - Target URL
30
+ * @param body - Form data as URLSearchParams
31
+ * @param options - Request options including agent and headers
32
+ * @returns Response promise
33
+ */
34
+ static postForm(url: string, body: URLSearchParams, options: FapiRequestOptions): Promise<Response>;
35
+ /**
36
+ * Makes a GET request with FAPI headers.
37
+ *
38
+ * @param url - Target URL
39
+ * @param options - Request options including agent and headers
40
+ * @returns Response promise
41
+ */
42
+ static get(url: string, options: FapiRequestOptions): Promise<Response>;
43
+ /**
44
+ * Makes a GET request with Bearer token authentication.
45
+ *
46
+ * @param url - Target URL
47
+ * @param accessToken - Bearer access token
48
+ * @param options - Request options including agent and headers
49
+ * @returns Response promise
50
+ */
51
+ static getWithToken(url: string, accessToken: string, options: FapiRequestOptions): Promise<Response>;
52
+ /**
53
+ * Parses a JSON response body.
54
+ *
55
+ * @param response - Fetch response
56
+ * @returns Parsed JSON object
57
+ * @throws Error if response is not OK or JSON parsing fails
58
+ */
59
+ static parseJsonResponse<T = unknown>(response: Response): Promise<T>;
60
+ }