@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.
- package/README.md +60 -71
- package/package.json +4 -5
- package/{config.js → src/config.js} +2 -31
- package/src/conformance/api/conformance-api.d.ts +38 -0
- package/src/conformance/api/conformance-api.js +53 -0
- package/src/conformance/config.json +60 -0
- package/src/conformance/conformance-config.d.ts +2 -0
- package/src/conformance/conformance-config.js +34 -0
- package/src/conformance/conformance.test.js +101 -0
- package/src/conformance/variant.json +1 -0
- package/src/crypto/crypto-loader.d.ts +32 -0
- package/src/crypto/crypto-loader.js +49 -0
- package/src/crypto/jwt-helper.d.ts +61 -0
- package/src/crypto/jwt-helper.js +92 -0
- package/src/crypto/pkce-helper.d.ts +43 -0
- package/src/crypto/pkce-helper.js +75 -0
- package/src/endpoints/participants-endpoint.d.ts +55 -0
- package/src/endpoints/participants-endpoint.js +137 -0
- package/src/endpoints/pushed-authorisation-request-endpoint.d.ts +87 -0
- package/src/endpoints/pushed-authorisation-request-endpoint.js +192 -0
- package/src/endpoints/retrieve-token-endpoint.d.ts +66 -0
- package/src/endpoints/retrieve-token-endpoint.js +159 -0
- package/src/endpoints/userinfo-endpoint.d.ts +24 -0
- package/src/endpoints/userinfo-endpoint.js +50 -0
- package/src/fapi/fapi-utils.d.ts +6 -0
- package/src/fapi/fapi-utils.js +9 -0
- package/src/http/http-client-extensions.d.ts +60 -0
- package/src/http/http-client-extensions.js +106 -0
- package/src/http/http-client-factory.d.ts +27 -0
- package/src/http/http-client-factory.js +45 -0
- package/src/integration/integration.test.d.ts +1 -0
- package/src/integration/integration.test.js +30 -0
- package/src/model/callback-params.d.ts +31 -0
- package/src/model/callback-params.js +1 -0
- package/src/model/claims.d.ts +100 -0
- package/src/model/claims.js +1 -0
- package/src/model/consolidated-token-set.d.ts +74 -0
- package/src/model/consolidated-token-set.js +100 -0
- package/src/model/discovery-service.d.ts +46 -0
- package/src/model/discovery-service.js +112 -0
- package/src/model/issuer-metadata.d.ts +165 -0
- package/src/model/issuer-metadata.js +1 -0
- package/src/model/jwks.d.ts +12 -0
- package/src/model/jwks.js +1 -0
- package/src/model/token-response.d.ts +31 -0
- package/src/model/token-response.js +1 -0
- package/src/model/token-set.d.ts +73 -0
- package/src/model/token-set.js +179 -0
- package/src/relying-party-client-sdk.d.ts +68 -0
- package/src/relying-party-client-sdk.js +150 -0
- package/src/test-data/large-participants-test-data.d.ts +865 -0
- package/src/test-data/large-participants-test-data.js +18907 -0
- package/src/test-data/participants-test-data.d.ts +149 -0
- package/src/test-data/participants-test-data.js +458 -0
- package/src/test-data/sandbox-participants-test-data.d.ts +865 -0
- package/src/test-data/sandbox-participants-test-data.js +3794 -0
- package/src/tests/cert-utils.test.d.ts +1 -0
- package/src/tests/cert-utils.test.js +13 -0
- package/src/tests/functional-utils.test.d.ts +1 -0
- package/src/tests/functional-utils.test.js +13 -0
- package/src/tests/participant-filters.test.d.ts +1 -0
- package/src/tests/participant-filters.test.js +151 -0
- package/src/tests/pushed-authorisation-request-endpoint.test.d.ts +1 -0
- package/src/tests/pushed-authorisation-request-endpoint.test.js +159 -0
- package/src/tests/relying-party-client-sdk.test.d.ts +1 -0
- package/src/tests/relying-party-client-sdk.test.js +313 -0
- package/src/tests/request-utils.test.d.ts +1 -0
- package/src/tests/request-utils.test.js +16 -0
- package/src/tests/system-information.test.d.ts +1 -0
- package/src/tests/system-information.test.js +16 -0
- package/src/tests/user-agent.test.d.ts +1 -0
- package/src/tests/user-agent.test.js +23 -0
- package/src/tests/validator.test.d.ts +1 -0
- package/src/tests/validator.test.js +38 -0
- package/{types.d.ts → src/types.d.ts} +61 -32
- package/src/types.js +1 -0
- package/{utils → src/utils}/request-utils.d.ts +1 -1
- package/src/utils/request-utils.js +8 -0
- package/{utils → src/utils}/user-agent.d.ts +1 -1
- package/{utils → src/utils}/user-agent.js +1 -1
- package/relying-party-client-sdk.d.ts +0 -37
- package/relying-party-client-sdk.js +0 -364
- package/utils/request-utils.js +0 -8
- /package/{config.d.ts → src/config.d.ts} +0 -0
- /package/{types.js → src/conformance/conformance.test.d.ts} +0 -0
- /package/{filter → src/filter}/participant-filters.d.ts +0 -0
- /package/{filter → src/filter}/participant-filters.js +0 -0
- /package/{logger.d.ts → src/logger.d.ts} +0 -0
- /package/{logger.js → src/logger.js} +0 -0
- /package/{utils → src/utils}/cert-utils.d.ts +0 -0
- /package/{utils → src/utils}/cert-utils.js +0 -0
- /package/{utils → src/utils}/functional-utils.d.ts +0 -0
- /package/{utils → src/utils}/functional-utils.js +0 -0
- /package/{utils → src/utils}/system-information.d.ts +0 -0
- /package/{utils → src/utils}/system-information.js +0 -0
- /package/{validator.d.ts → src/validator.d.ts} +0 -0
- /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,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
|
+
}
|