@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,101 @@
|
|
|
1
|
+
import RelyingPartyClientSdk from '../relying-party-client-sdk.js';
|
|
2
|
+
import { conformanceConfig } from './conformance-config.js';
|
|
3
|
+
import config from './config.json';
|
|
4
|
+
import variant from './variant.json';
|
|
5
|
+
import packageJson from '../../package.json';
|
|
6
|
+
import { parse } from 'url';
|
|
7
|
+
import { ConformanceApi } from './api/conformance-api.js';
|
|
8
|
+
import winston from 'winston';
|
|
9
|
+
import { describe, before, it } from 'node:test';
|
|
10
|
+
import assert from 'node:assert';
|
|
11
|
+
const alias = 'conformance-nodejs-' + Date.now();
|
|
12
|
+
const conformanceEnv = process.env.CONFORMANCE_ENV || 'production';
|
|
13
|
+
const conformanceApiToken = process.env.CONFORMANCE_API_TOKEN || '';
|
|
14
|
+
const conformanceNodeVersion = process.env.CONFORMANCE_NODE_VERSION || 'unknown';
|
|
15
|
+
const participantsEnv = conformanceEnv === 'production' ? '' : conformanceEnv;
|
|
16
|
+
conformanceConfig.data.registry_participants_uri = `https://api.sandbox.connectid.com.au/oidf-conformance/participants?alias=a/${alias}&env=${participantsEnv}`;
|
|
17
|
+
const rpClient = new RelyingPartyClientSdk(conformanceConfig);
|
|
18
|
+
const conformanceEnvUrls = new Map([
|
|
19
|
+
['staging', 'https://staging.certification.openid.net/'],
|
|
20
|
+
['production', 'https://www.certification.openid.net/']
|
|
21
|
+
]);
|
|
22
|
+
const conformanceBaseUrl = conformanceEnvUrls.get(conformanceEnv);
|
|
23
|
+
console.log(`Selected environment ${conformanceEnv}, baseUrl: ${conformanceBaseUrl}`);
|
|
24
|
+
const conformanceApi = new ConformanceApi(conformanceApiToken, conformanceBaseUrl);
|
|
25
|
+
describe('Conformance Test', { timeout: 600000 }, () => {
|
|
26
|
+
let planInfo;
|
|
27
|
+
before(async () => {
|
|
28
|
+
const planName = 'fapi2-message-signing-id1-client-test-plan';
|
|
29
|
+
config.description = `NodeJS SDK ${packageJson.version} (NodeJS v${conformanceNodeVersion})`;
|
|
30
|
+
config.alias = alias;
|
|
31
|
+
planInfo = await conformanceApi.createPlan(planName, JSON.stringify(variant), config);
|
|
32
|
+
});
|
|
33
|
+
it('should execute the conformance test', async () => {
|
|
34
|
+
const testModules = planInfo.modules.map((module) => module.testModule);
|
|
35
|
+
for (const testName of testModules) {
|
|
36
|
+
console.log(`Executing test ${testName}`);
|
|
37
|
+
// We are mimicking an application log here.
|
|
38
|
+
const logger = createTestLogger(testName);
|
|
39
|
+
rpClient.logger = logger;
|
|
40
|
+
const testInstance = await conformanceApi.createTestFromPlan(planInfo.id, testName);
|
|
41
|
+
let testInformation = await conformanceApi.getTestInformation(testInstance.id);
|
|
42
|
+
logger.info(`Executing version ${testInformation.version} of ${testName}`);
|
|
43
|
+
console.log(`See ${conformanceBaseUrl}log-detail.html?log=${testInstance.id}`);
|
|
44
|
+
const idps = await rpClient.getParticipants();
|
|
45
|
+
const authorisationServerId = idps[0].AuthorisationServers[0].AuthorisationServerId;
|
|
46
|
+
logger.info(`Sending PAR to ${authorisationServerId}`);
|
|
47
|
+
const essentialClaims = ['given_name', 'middle_name', 'family_name', 'phone_number', 'email', 'address', 'birthdate', 'txn'];
|
|
48
|
+
const parResponse = await rpClient.sendPushedAuthorisationRequest(authorisationServerId, essentialClaims);
|
|
49
|
+
const response = await fetch(parResponse.authUrl, {
|
|
50
|
+
redirect: 'manual',
|
|
51
|
+
});
|
|
52
|
+
if (response.status !== 303) {
|
|
53
|
+
console.error(`Expected 303, got ${response.status}`);
|
|
54
|
+
throw new Error(`Expected 303, got ${response.status}`);
|
|
55
|
+
}
|
|
56
|
+
const location = response.headers.get('location');
|
|
57
|
+
if (!location) {
|
|
58
|
+
throw new Error('No location header');
|
|
59
|
+
}
|
|
60
|
+
const locationObj = parse(location, true);
|
|
61
|
+
logger.info(`Executing ${testName}`);
|
|
62
|
+
try {
|
|
63
|
+
logger.info('Getting tokens');
|
|
64
|
+
await rpClient.retrieveTokens(authorisationServerId, locationObj.query, parResponse.codeVerifier, parResponse.state, parResponse.nonce);
|
|
65
|
+
logger.info('Tokens successfully retrieved');
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
logger.error('An error occured while getting tokens:' + getErrorMessage(e));
|
|
69
|
+
console.log(e);
|
|
70
|
+
}
|
|
71
|
+
while (testInformation.status !== 'FINISHED') {
|
|
72
|
+
await delay(250);
|
|
73
|
+
testInformation = await conformanceApi.getTestInformation(testInstance.id);
|
|
74
|
+
}
|
|
75
|
+
logger.info(`Test finished with result: ${testInformation.result}, status: ${testInformation.status}`);
|
|
76
|
+
console.log('Full test information:', JSON.stringify(testInformation, null, 2));
|
|
77
|
+
assert.ok(['WARNING', 'PASSED'].includes(testInformation.result));
|
|
78
|
+
if (testInformation.result === 'WARNING') {
|
|
79
|
+
console.log(`*** A warning occurred while executing ${testName}, please check the logs!!! ***`);
|
|
80
|
+
}
|
|
81
|
+
// TODO: download the logs and store them in Github
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
function delay(time) {
|
|
86
|
+
// @ts-ignore
|
|
87
|
+
return new Promise((resolve) => setTimeout(resolve, time));
|
|
88
|
+
}
|
|
89
|
+
function getErrorMessage(error) {
|
|
90
|
+
if (error instanceof Error)
|
|
91
|
+
return error.message;
|
|
92
|
+
return String(error);
|
|
93
|
+
}
|
|
94
|
+
function createTestLogger(testName) {
|
|
95
|
+
return winston.createLogger({
|
|
96
|
+
level: 'info',
|
|
97
|
+
format: winston.format.simple(),
|
|
98
|
+
defaultMeta: { service: 'conformance-test' },
|
|
99
|
+
transports: [new winston.transports.File({ filename: `logs/${testName}.log` })],
|
|
100
|
+
});
|
|
101
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "client_auth_type": "private_key_jwt", "fapi_request_method": "signed_non_repudiation", "fapi_client_type": "oidc", "sender_constrain": "mtls", "fapi_profile": "connectid_au", "fapi_response_mode": "plain_response" }
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { KeyObject } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* Utility class for loading cryptographic keys and certificates.
|
|
4
|
+
*
|
|
5
|
+
* Handles conversion from PEM format to Node.js crypto objects.
|
|
6
|
+
*/
|
|
7
|
+
export declare class CryptoLoader {
|
|
8
|
+
/**
|
|
9
|
+
* Loads a private key from PEM format.
|
|
10
|
+
*
|
|
11
|
+
* @param keyPem - Private key in PEM format (string or Buffer)
|
|
12
|
+
* @returns KeyObject that can be used for signing operations
|
|
13
|
+
* @throws Error if the key cannot be loaded
|
|
14
|
+
*/
|
|
15
|
+
static loadPrivateKey(keyPem: string | Buffer): KeyObject;
|
|
16
|
+
/**
|
|
17
|
+
* Loads a certificate from PEM format.
|
|
18
|
+
*
|
|
19
|
+
* @param certPem - Certificate in PEM format (string or Buffer)
|
|
20
|
+
* @returns Certificate as string
|
|
21
|
+
*/
|
|
22
|
+
static loadCertificate(certPem: string | Buffer): string;
|
|
23
|
+
/**
|
|
24
|
+
* Loads a certificate chain from PEM format.
|
|
25
|
+
*
|
|
26
|
+
* Handles single or multiple certificates in a PEM bundle.
|
|
27
|
+
*
|
|
28
|
+
* @param caPem - CA certificate(s) in PEM format (string or Buffer)
|
|
29
|
+
* @returns Array of certificates as strings
|
|
30
|
+
*/
|
|
31
|
+
static loadCertificateChain(caPem: string | Buffer): string[];
|
|
32
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createPrivateKey } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* Utility class for loading cryptographic keys and certificates.
|
|
4
|
+
*
|
|
5
|
+
* Handles conversion from PEM format to Node.js crypto objects.
|
|
6
|
+
*/
|
|
7
|
+
export class CryptoLoader {
|
|
8
|
+
/**
|
|
9
|
+
* Loads a private key from PEM format.
|
|
10
|
+
*
|
|
11
|
+
* @param keyPem - Private key in PEM format (string or Buffer)
|
|
12
|
+
* @returns KeyObject that can be used for signing operations
|
|
13
|
+
* @throws Error if the key cannot be loaded
|
|
14
|
+
*/
|
|
15
|
+
static loadPrivateKey(keyPem) {
|
|
16
|
+
try {
|
|
17
|
+
return createPrivateKey(keyPem);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
throw new Error(`Failed to load private key: ${error instanceof Error ? error.message : String(error)}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Loads a certificate from PEM format.
|
|
25
|
+
*
|
|
26
|
+
* @param certPem - Certificate in PEM format (string or Buffer)
|
|
27
|
+
* @returns Certificate as string
|
|
28
|
+
*/
|
|
29
|
+
static loadCertificate(certPem) {
|
|
30
|
+
return typeof certPem === 'string' ? certPem : certPem.toString('utf-8');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Loads a certificate chain from PEM format.
|
|
34
|
+
*
|
|
35
|
+
* Handles single or multiple certificates in a PEM bundle.
|
|
36
|
+
*
|
|
37
|
+
* @param caPem - CA certificate(s) in PEM format (string or Buffer)
|
|
38
|
+
* @returns Array of certificates as strings
|
|
39
|
+
*/
|
|
40
|
+
static loadCertificateChain(caPem) {
|
|
41
|
+
const pemString = typeof caPem === 'string' ? caPem : caPem.toString('utf-8');
|
|
42
|
+
// Split on BEGIN CERTIFICATE markers to handle certificate chains
|
|
43
|
+
const certificates = pemString
|
|
44
|
+
.split(/(?=-----BEGIN CERTIFICATE-----)/)
|
|
45
|
+
.map((cert) => cert.trim())
|
|
46
|
+
.filter((cert) => cert.length > 0);
|
|
47
|
+
return certificates;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { KeyObject } from 'node:crypto';
|
|
2
|
+
import { ClaimsRequest } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Parameters for generating a request JWT (PAR request object).
|
|
5
|
+
*/
|
|
6
|
+
export interface RequestJwtParams {
|
|
7
|
+
issuer: string;
|
|
8
|
+
audience: string;
|
|
9
|
+
redirectUri: string;
|
|
10
|
+
scope: string;
|
|
11
|
+
responseType: string;
|
|
12
|
+
codeChallenge: string;
|
|
13
|
+
codeChallengeMethod: string;
|
|
14
|
+
state: string;
|
|
15
|
+
nonce: string;
|
|
16
|
+
claims: ClaimsRequest;
|
|
17
|
+
purpose: string;
|
|
18
|
+
prompt: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Helper class for JWT operations.
|
|
22
|
+
*
|
|
23
|
+
* Handles creation and signing of JWTs for:
|
|
24
|
+
* - Request objects (PAR)
|
|
25
|
+
* - Client assertions (token endpoint authentication)
|
|
26
|
+
*
|
|
27
|
+
* Uses the PS256 algorithm (RSA-PSS with SHA-256) as required by FAPI.
|
|
28
|
+
*/
|
|
29
|
+
export declare class JwtHelper {
|
|
30
|
+
private readonly signingKey;
|
|
31
|
+
private readonly signingKid;
|
|
32
|
+
private readonly clientId;
|
|
33
|
+
/**
|
|
34
|
+
* Creates a new JwtHelper instance.
|
|
35
|
+
*
|
|
36
|
+
* @param signingKey - Private key for signing JWTs
|
|
37
|
+
* @param signingKid - Key ID to include in JWT header
|
|
38
|
+
* @param clientId - OAuth client ID
|
|
39
|
+
*/
|
|
40
|
+
constructor(signingKey: KeyObject, signingKid: string, clientId: string);
|
|
41
|
+
/**
|
|
42
|
+
* Generates a signed request JWT for PAR.
|
|
43
|
+
*
|
|
44
|
+
* The request object contains all authorization request parameters
|
|
45
|
+
* and is signed to prevent tampering.
|
|
46
|
+
*
|
|
47
|
+
* @param params - Request parameters
|
|
48
|
+
* @returns Signed JWT string
|
|
49
|
+
*/
|
|
50
|
+
generateRequestJwt(params: RequestJwtParams): Promise<string>;
|
|
51
|
+
/**
|
|
52
|
+
* Generates a client assertion JWT for token endpoint authentication.
|
|
53
|
+
*
|
|
54
|
+
* The client assertion proves the client's identity using JWT-based
|
|
55
|
+
* authentication (private_key_jwt method).
|
|
56
|
+
*
|
|
57
|
+
* @param audience - Token endpoint URL (or issuer)
|
|
58
|
+
* @returns Signed JWT string
|
|
59
|
+
*/
|
|
60
|
+
generateClientAssertionJwt(audience: string): Promise<string>;
|
|
61
|
+
}
|
|
@@ -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
|
+
}
|