@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
@@ -0,0 +1,92 @@
1
+ import { SignJWT } from 'jose';
2
+ import { randomUUID } from 'node:crypto';
3
+ /**
4
+ * Helper class for JWT operations.
5
+ *
6
+ * Handles creation and signing of JWTs for:
7
+ * - Request objects (PAR)
8
+ * - Client assertions (token endpoint authentication)
9
+ *
10
+ * Uses the PS256 algorithm (RSA-PSS with SHA-256) as required by FAPI.
11
+ */
12
+ export class JwtHelper {
13
+ /**
14
+ * Creates a new JwtHelper instance.
15
+ *
16
+ * @param signingKey - Private key for signing JWTs
17
+ * @param signingKid - Key ID to include in JWT header
18
+ * @param clientId - OAuth client ID
19
+ */
20
+ constructor(signingKey, signingKid, clientId) {
21
+ this.signingKey = signingKey;
22
+ this.signingKid = signingKid;
23
+ this.clientId = clientId;
24
+ }
25
+ /**
26
+ * Generates a signed request JWT for PAR.
27
+ *
28
+ * The request object contains all authorization request parameters
29
+ * and is signed to prevent tampering.
30
+ *
31
+ * @param params - Request parameters
32
+ * @returns Signed JWT string
33
+ */
34
+ async generateRequestJwt(params) {
35
+ const now = Math.floor(Date.now() / 1000);
36
+ return new SignJWT({
37
+ // OAuth/OIDC parameters
38
+ iss: this.clientId,
39
+ aud: params.audience,
40
+ client_id: this.clientId,
41
+ scope: params.scope,
42
+ response_type: params.responseType,
43
+ redirect_uri: params.redirectUri,
44
+ // PKCE
45
+ code_challenge: params.codeChallenge,
46
+ code_challenge_method: params.codeChallengeMethod,
47
+ // OIDC parameters
48
+ state: params.state,
49
+ nonce: params.nonce,
50
+ claims: params.claims,
51
+ prompt: params.prompt,
52
+ // ConnectID extension
53
+ purpose: params.purpose,
54
+ // JWT metadata
55
+ jti: randomUUID(),
56
+ })
57
+ .setProtectedHeader({
58
+ alg: 'PS256',
59
+ kid: this.signingKid,
60
+ typ: 'oauth-authz-req+jwt',
61
+ })
62
+ .setIssuedAt(now)
63
+ .setNotBefore(now) // nbf = iat (required by FAPI)
64
+ .setExpirationTime(now + 300) // 5 minutes
65
+ .sign(this.signingKey);
66
+ }
67
+ /**
68
+ * Generates a client assertion JWT for token endpoint authentication.
69
+ *
70
+ * The client assertion proves the client's identity using JWT-based
71
+ * authentication (private_key_jwt method).
72
+ *
73
+ * @param audience - Token endpoint URL (or issuer)
74
+ * @returns Signed JWT string
75
+ */
76
+ async generateClientAssertionJwt(audience) {
77
+ const now = Math.floor(Date.now() / 1000);
78
+ return new SignJWT({
79
+ iss: this.clientId,
80
+ sub: this.clientId,
81
+ aud: audience,
82
+ jti: randomUUID(),
83
+ })
84
+ .setProtectedHeader({
85
+ alg: 'PS256',
86
+ kid: this.signingKid,
87
+ })
88
+ .setIssuedAt(now)
89
+ .setExpirationTime(now + 300) // 5 minutes
90
+ .sign(this.signingKey);
91
+ }
92
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Helper class for PKCE (Proof Key for Code Exchange) operations.
3
+ *
4
+ * Implements RFC 7636 with S256 code challenge method.
5
+ * All random values use cryptographically secure random number generation.
6
+ */
7
+ export declare class PkceHelper {
8
+ /**
9
+ * Generates a cryptographically random state parameter.
10
+ *
11
+ * Used to prevent CSRF attacks during the authorization flow.
12
+ *
13
+ * @returns Base64URL-encoded random string (48 bytes -> ~64 characters)
14
+ */
15
+ static generateState(): string;
16
+ /**
17
+ * Generates a cryptographically random nonce.
18
+ *
19
+ * Used to associate a client session with an ID token and mitigate replay attacks.
20
+ *
21
+ * @returns Base64URL-encoded random string (43 characters)
22
+ */
23
+ static generateNonce(): string;
24
+ /**
25
+ * Generates a code verifier for PKCE.
26
+ *
27
+ * The code verifier is a cryptographically random string that is used to
28
+ * correlate the authorization request with the token request.
29
+ *
30
+ * @returns Base64URL-encoded random string (43-128 characters)
31
+ */
32
+ static generateCodeVerifier(): string;
33
+ /**
34
+ * Generates a code challenge from a code verifier using S256 method.
35
+ *
36
+ * The code challenge is sent in the authorization request, and the
37
+ * code verifier is sent in the token request.
38
+ *
39
+ * @param codeVerifier - The code verifier to hash
40
+ * @returns Base64URL-encoded SHA256 hash of the code verifier
41
+ */
42
+ static generateCodeChallenge(codeVerifier: string): string;
43
+ }
@@ -0,0 +1,75 @@
1
+ import { randomBytes, createHash } from 'node:crypto';
2
+ /**
3
+ * Helper class for PKCE (Proof Key for Code Exchange) operations.
4
+ *
5
+ * Implements RFC 7636 with S256 code challenge method.
6
+ * All random values use cryptographically secure random number generation.
7
+ */
8
+ export class PkceHelper {
9
+ /**
10
+ * Generates a cryptographically random state parameter.
11
+ *
12
+ * Used to prevent CSRF attacks during the authorization flow.
13
+ *
14
+ * @returns Base64URL-encoded random string (48 bytes -> ~64 characters)
15
+ */
16
+ static generateState() {
17
+ // 48 bytes of random data -> 64 chars in base64url
18
+ return base64urlEncode(randomBytes(48));
19
+ }
20
+ /**
21
+ * Generates a cryptographically random nonce.
22
+ *
23
+ * Used to associate a client session with an ID token and mitigate replay attacks.
24
+ *
25
+ * @returns Base64URL-encoded random string (43 characters)
26
+ */
27
+ static generateNonce() {
28
+ // Generate 32 bytes and take first 43 chars (OIDC spec requirement)
29
+ return base64urlEncode(randomBytes(32)).substring(0, 43);
30
+ }
31
+ /**
32
+ * Generates a code verifier for PKCE.
33
+ *
34
+ * The code verifier is a cryptographically random string that is used to
35
+ * correlate the authorization request with the token request.
36
+ *
37
+ * @returns Base64URL-encoded random string (43-128 characters)
38
+ */
39
+ static generateCodeVerifier() {
40
+ // 32 bytes -> 43 chars in base64url (minimum required by spec)
41
+ return base64urlEncode(randomBytes(32));
42
+ }
43
+ /**
44
+ * Generates a code challenge from a code verifier using S256 method.
45
+ *
46
+ * The code challenge is sent in the authorization request, and the
47
+ * code verifier is sent in the token request.
48
+ *
49
+ * @param codeVerifier - The code verifier to hash
50
+ * @returns Base64URL-encoded SHA256 hash of the code verifier
51
+ */
52
+ static generateCodeChallenge(codeVerifier) {
53
+ // SHA256 hash the code verifier and base64url encode it
54
+ const hash = createHash('sha256').update(codeVerifier).digest();
55
+ return base64urlEncode(hash);
56
+ }
57
+ }
58
+ /**
59
+ * Encodes a buffer to base64url format (RFC 4648).
60
+ *
61
+ * Base64URL is base64 with URL-safe characters:
62
+ * - '+' becomes '-'
63
+ * - '/' becomes '_'
64
+ * - Padding '=' is removed
65
+ *
66
+ * @param buffer - Data to encode
67
+ * @returns Base64URL-encoded string
68
+ */
69
+ function base64urlEncode(buffer) {
70
+ return buffer
71
+ .toString('base64')
72
+ .replace(/\+/g, '-')
73
+ .replace(/\//g, '_')
74
+ .replace(/=/g, '');
75
+ }
@@ -0,0 +1,55 @@
1
+ import { Agent } from 'undici';
2
+ import { Logger } from 'winston';
3
+ import { AuthorisationServer, Participant, RelyingPartyClientSdkConfig } from '../types.js';
4
+ import ParticipantFilters from '../filter/participant-filters.js';
5
+ /**
6
+ * Participants Endpoint
7
+ *
8
+ * Handles fetching and filtering of participant lists from the registry.
9
+ * Does NOT cache - fetches fresh data on every call
10
+ */
11
+ export declare class ParticipantsEndpoint {
12
+ private readonly sdkConfig;
13
+ private readonly participantFilters;
14
+ private readonly httpClient;
15
+ private readonly logger;
16
+ private readonly getCurrentDate;
17
+ constructor(sdkConfig: RelyingPartyClientSdkConfig, participantFilters: ParticipantFilters, httpClient: Agent, logger: Logger, getCurrentDate: () => Date);
18
+ /**
19
+ * Retrieves the list of active participants.
20
+ *
21
+ * Applies filtering based on SDK configuration:
22
+ * - Removes fallback identity service provider
23
+ * - Filters by certification status (if not include_uncertified_participants)
24
+ * - Filters by required claims (if specified)
25
+ * - Filters by required certifications (if specified)
26
+ * - Removes inactive auth servers
27
+ * - Removes participants without auth servers
28
+ *
29
+ * @returns Filtered list of participants
30
+ */
31
+ getParticipants(): Promise<Participant[]>;
32
+ /**
33
+ * Retrieves the list of fallback provider participants.
34
+ *
35
+ * Returns ONLY the fallback identity service provider participants.
36
+ * Applies certification filtering before returning.
37
+ *
38
+ * @returns List of fallback provider participants
39
+ */
40
+ getFallbackProviderParticipants(): Promise<Participant[]>;
41
+ /**
42
+ * Fetches participants from the registry.
43
+ *
44
+ * @param uri - Registry participants URI
45
+ * @returns Raw list of participants
46
+ */
47
+ fetchParticipants(uri: string): Promise<Participant[]>;
48
+ /**
49
+ * Gets authorization server details by ID.
50
+ *
51
+ * @param authServerId - Authorization server ID
52
+ * @returns Authorization server details
53
+ */
54
+ getAuthServerDetails(authServerId: string): Promise<AuthorisationServer>;
55
+ }
@@ -0,0 +1,137 @@
1
+ import { HttpClientExtensions } from '../http/http-client-extensions.js';
2
+ import { randomUUID } from 'node:crypto';
3
+ /**
4
+ * Participants Endpoint
5
+ *
6
+ * Handles fetching and filtering of participant lists from the registry.
7
+ * Does NOT cache - fetches fresh data on every call
8
+ */
9
+ export class ParticipantsEndpoint {
10
+ constructor(sdkConfig, participantFilters, httpClient, logger, getCurrentDate) {
11
+ this.sdkConfig = sdkConfig;
12
+ this.participantFilters = participantFilters;
13
+ this.httpClient = httpClient;
14
+ this.logger = logger;
15
+ this.getCurrentDate = getCurrentDate;
16
+ }
17
+ /**
18
+ * Retrieves the list of active participants.
19
+ *
20
+ * Applies filtering based on SDK configuration:
21
+ * - Removes fallback identity service provider
22
+ * - Filters by certification status (if not include_uncertified_participants)
23
+ * - Filters by required claims (if specified)
24
+ * - Filters by required certifications (if specified)
25
+ * - Removes inactive auth servers
26
+ * - Removes participants without auth servers
27
+ *
28
+ * @returns Filtered list of participants
29
+ */
30
+ async getParticipants() {
31
+ const participantsUri = this.sdkConfig.data.registry_participants_uri;
32
+ try {
33
+ const participants = await this.fetchParticipants(this.sdkConfig.data.registry_participants_uri);
34
+ this.logger.info(`Retrieved identity providers from ${participantsUri}, num orgs found: ${participants.length}`);
35
+ // Remove fallback identity service provider
36
+ let filteredParticipants = this.participantFilters.removeFallbackIdentityServiceProvider(participants);
37
+ // If include_uncertified_participants is true, skip certification filtering
38
+ if (this.sdkConfig.data.include_uncertified_participants) {
39
+ this.logger.info('Include uncertified participants enabled - skipping certification filters');
40
+ filteredParticipants = this.participantFilters.removeParticipantsWithoutAuthServers(filteredParticipants);
41
+ this.logger.info(`Returning ${filteredParticipants.length} participants (unfiltered)`);
42
+ return filteredParticipants;
43
+ }
44
+ // Apply certification filtering
45
+ filteredParticipants = this.participantFilters.removeOutOfDateCertifications(filteredParticipants, this.getCurrentDate());
46
+ filteredParticipants = this.participantFilters.removeUnofficialCertifications(filteredParticipants);
47
+ // Apply required claims filtering
48
+ if (this.sdkConfig.data.required_claims) {
49
+ this.logger.debug(`Identity provider list filtered for participants that support the following claims: ${JSON.stringify(this.sdkConfig.data.required_claims)}`);
50
+ filteredParticipants = this.participantFilters.filterAuthServersForSupportedClaims(filteredParticipants, this.sdkConfig.data.required_claims);
51
+ }
52
+ // Apply required certifications filtering
53
+ if (this.sdkConfig.data.required_participant_certifications) {
54
+ this.logger.debug(`Identity provider list filtered for participants that support the following certifications: ${JSON.stringify(this.sdkConfig.data.required_participant_certifications)}`);
55
+ filteredParticipants = this.participantFilters.filterForRequiredCertifications(filteredParticipants, this.sdkConfig.data.required_participant_certifications);
56
+ }
57
+ filteredParticipants = this.participantFilters.removeInactiveAuthServers(filteredParticipants);
58
+ filteredParticipants = this.participantFilters.removeParticipantsWithoutAuthServers(filteredParticipants);
59
+ this.logger.info(`Returning ${filteredParticipants.length} participants after filtering`);
60
+ return filteredParticipants;
61
+ }
62
+ catch (err) {
63
+ throw new Error(`Unable to get participants from ${participantsUri}, ${err}`);
64
+ }
65
+ }
66
+ /**
67
+ * Retrieves the list of fallback provider participants.
68
+ *
69
+ * Returns ONLY the fallback identity service provider participants.
70
+ * Applies certification filtering before returning.
71
+ *
72
+ * @returns List of fallback provider participants
73
+ */
74
+ async getFallbackProviderParticipants() {
75
+ const participantsUri = this.sdkConfig.data.registry_participants_uri;
76
+ try {
77
+ const participants = await this.fetchParticipants(participantsUri);
78
+ this.logger.info(`Retrieved identity providers from ${participantsUri}, num orgs found: ${participants.length}`);
79
+ // Apply certification filtering
80
+ let filteredParticipants = this.participantFilters.removeOutOfDateCertifications(participants, this.getCurrentDate());
81
+ filteredParticipants = this.participantFilters.removeUnofficialCertifications(filteredParticipants);
82
+ filteredParticipants = this.participantFilters.filterForFallbackIdentityServiceProviders(filteredParticipants);
83
+ filteredParticipants = this.participantFilters.removeParticipantsWithoutAuthServers(filteredParticipants);
84
+ this.logger.info(`Returning ${filteredParticipants.length} fallback provider participants`);
85
+ return filteredParticipants;
86
+ }
87
+ catch (err) {
88
+ throw new Error(`Unable to get participants from ${participantsUri}, ${err}`);
89
+ }
90
+ }
91
+ /**
92
+ * Fetches participants from the registry.
93
+ *
94
+ * @param uri - Registry participants URI
95
+ * @returns Raw list of participants
96
+ */
97
+ async fetchParticipants(uri) {
98
+ try {
99
+ const response = await HttpClientExtensions.get(uri, {
100
+ agent: this.httpClient,
101
+ clientId: this.sdkConfig.data.client_id,
102
+ xFapiInteractionId: randomUUID(),
103
+ });
104
+ return await HttpClientExtensions.parseJsonResponse(response);
105
+ }
106
+ catch (error) {
107
+ throw new Error(`Failed to fetch participants from ${uri}: ${error instanceof Error ? error.message : String(error)}`);
108
+ }
109
+ }
110
+ /**
111
+ * Gets authorization server details by ID.
112
+ *
113
+ * @param authServerId - Authorization server ID
114
+ * @returns Authorization server details
115
+ */
116
+ async getAuthServerDetails(authServerId) {
117
+ // First, check in regular participants
118
+ const participantList = await this.getParticipants();
119
+ const servers = participantList.map(({ AuthorisationServers }) => AuthorisationServers).flat();
120
+ let found = servers.find(({ AuthorisationServerId }) => AuthorisationServerId === authServerId);
121
+ if (!found) {
122
+ // Check if it is one of the fallback servers
123
+ const fallbackIdps = await this.getFallbackProviderParticipants();
124
+ const fallbackServers = fallbackIdps.map(({ AuthorisationServers }) => AuthorisationServers).flat();
125
+ found = fallbackServers.find(({ AuthorisationServerId }) => AuthorisationServerId === authServerId);
126
+ if (!found) {
127
+ // Let the user know if it was an auth server that was filtered out
128
+ const allParticipants = await this.fetchParticipants(this.sdkConfig.data.registry_participants_uri);
129
+ const allAuthServers = allParticipants.map(({ AuthorisationServers }) => AuthorisationServers).flat();
130
+ const exists = allAuthServers.find(({ AuthorisationServerId }) => AuthorisationServerId === authServerId);
131
+ const additionalLogInfo = exists ? ` Server exists but was filtered from results due to config settings for 'include_uncertified_participants', 'required_participant_certifications' and 'required_claims'` : '';
132
+ throw new Error(`Unable to find specified Authorisation Server: ${authServerId}${additionalLogInfo}`);
133
+ }
134
+ }
135
+ return found;
136
+ }
137
+ }
@@ -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'];