UNPKG

@sphereon/oid4vci-client

Version:

OpenID for Verifiable Credential Issuance (OpenID4VCI) client

281 lines (255 loc) • 13.8 kB
import { CodeChallengeMethod, CredentialOfferPayloadV1_0_13, determineSpecVersionFromOffer, determineSpecVersionFromURI, OpenId4VCIVersion, WellKnownEndpoints, } from '@sphereon/oid4vci-common'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore import nock from 'nock'; import { createCredentialOfferURIFromObject } from '../../../issuer/lib'; import { OpenID4VCIClient } from '../OpenID4VCIClient'; const MOCK_URL = 'https://server.example.com/'; describe('OpenID4VCIClient should', () => { let client: OpenID4VCIClient; beforeEach(async () => { nock(MOCK_URL).get(/.*/).reply(200, {}); nock(MOCK_URL).get(WellKnownEndpoints.OAUTH_AS).reply(404, {}); nock(MOCK_URL).get(WellKnownEndpoints.OPENID_CONFIGURATION).reply(404, {}); client = await OpenID4VCIClient.fromURI({ clientId: 'test-client', uri: 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fserver.example.com%22%2C%22credential_configuration_ids%22%3A%5B%22TestCredential%22%5D%7D', createAuthorizationRequestURL: false, }); }); afterEach(() => { nock.cleanAll(); }); it('should successfully construct an authorization request url', async () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore client._state.endpointMetadata?.credentialIssuerMetadata.authorization_endpoint = `${MOCK_URL}v1/auth/authorize`; const url = await client.createAuthorizationRequestUrl({ authorizationRequest: { scope: 'openid TestCredential', redirectUri: 'http://localhost:8881/cb', }, }); const urlSearchParams = new URLSearchParams(url.split('?')[1]); const scope = urlSearchParams.get('scope')?.split(' '); expect(scope?.[0]).toBe('openid'); }); it('throw an error if authorization endpoint is not set in server metadata', async () => { await expect( client.createAuthorizationRequestUrl({ authorizationRequest: { scope: 'openid TestCredential', redirectUri: 'http://localhost:8881/cb', }, }), ).rejects.toThrow(Error('Server metadata does not contain authorization endpoint')); }); it('throw an error if no scope and no authorization_details is provided', async () => { nock(MOCK_URL).get(/.*/).reply(200, {}); nock(MOCK_URL).get(WellKnownEndpoints.OAUTH_AS).reply(200, {}); nock(MOCK_URL).get(WellKnownEndpoints.OPENID_CONFIGURATION).reply(200, {}); // Use a client with issuer only to trigger the error client = await OpenID4VCIClient.fromCredentialIssuer({ credentialIssuer: MOCK_URL, createAuthorizationRequestURL: false, retrieveServerMetadata: false, }); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore client._state.endpointMetadata = { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore credentialIssuerMetadata: { authorization_endpoint: `${MOCK_URL}v1/auth/authorize`, token_endpoint: `${MOCK_URL}/token`, }, }; // client._state.endpointMetadata.credentialIssuerMetadata.authorization_endpoint = `${MOCK_URL}v1/auth/authorize`; await expect( client.createAuthorizationRequestUrl({ pkce: { codeChallengeMethod: CodeChallengeMethod.S256, codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs', }, authorizationRequest: { clientId: 'clientId', redirectUri: 'http://localhost:8881/cb', }, }), ).rejects.toThrow(Error('Please provide a scope or authorization_details if no credential offer is present')); }); it('create an authorization request url with authorization_details array property', async () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore client._state.endpointMetadata?.credentialIssuerMetadata.authorization_endpoint = `${MOCK_URL}v1/auth/authorize`; await expect( client.createAuthorizationRequestUrl({ pkce: { codeChallengeMethod: CodeChallengeMethod.S256, codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs', }, authorizationRequest: { authorizationDetails: [ { type: 'openid_credential', format: 'ldp_vc', credential_definition: { '@context': ['https://www.w3.org/2018/credentials/v1', 'https://www.w3.org/2018/credentials/examples/v1'], types: ['VerifiableCredential', 'UniversityDegreeCredential'], }, }, { type: 'openid_credential', // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore format: 'mso_mdoc', doctype: 'org.iso.18013.5.1.mDL', }, ], redirectUri: 'http://localhost:8881/cb', }, }), ).resolves.toEqual( 'https://server.example.com/v1/auth/authorize?response_type=code&code_challenge_method=S256&code_challenge=mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs&authorization_details=%5B%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22ldp_vc%22%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%2C%22locations%22%3A%5B%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%5D%7D%2C%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22mso_mdoc%22%2C%22doctype%22%3A%22org%2Eiso%2E18013%2E5%2E1%2EmDL%22%2C%22locations%22%3A%5B%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%5D%7D%5D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb&client_id=test-client', ); }); it('create an authorization request url with authorization_details object property', async () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore client._state.endpointMetadata?.credentialIssuerMetadata.authorization_endpoint = `${MOCK_URL}v1/auth/authorize`; await expect( client.createAuthorizationRequestUrl({ pkce: { codeChallengeMethod: CodeChallengeMethod.S256, codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs', }, authorizationRequest: { authorizationDetails: { type: 'openid_credential', format: 'ldp_vc', credential_definition: { '@context': ['https://www.w3.org/2018/credentials/v1', 'https://www.w3.org/2018/credentials/examples/v1'], types: ['VerifiableCredential', 'UniversityDegreeCredential'], }, }, redirectUri: 'http://localhost:8881/cb', }, }), ).resolves.toEqual( 'https://server.example.com/v1/auth/authorize?response_type=code&code_challenge_method=S256&code_challenge=mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs&authorization_details=%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22ldp_vc%22%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%2C%22locations%22%3A%5B%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%5D%7D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb&client_id=test-client', ); }); it('create an authorization request url with authorization_details and scope', async () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore client._state.endpointMetadata.credentialIssuerMetadata.authorization_endpoint = `${MOCK_URL}v1/auth/authorize`; await expect( client.createAuthorizationRequestUrl({ pkce: { codeChallengeMethod: CodeChallengeMethod.S256, codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs', }, authorizationRequest: { authorizationDetails: { type: 'openid_credential', format: 'ldp_vc', locations: ['https://test.com'], credential_definition: { '@context': ['https://www.w3.org/2018/credentials/v1', 'https://www.w3.org/2018/credentials/examples/v1'], types: ['VerifiableCredential', 'UniversityDegreeCredential'], }, }, scope: 'openid', redirectUri: 'http://localhost:8881/cb', }, }), ).resolves.toEqual( 'https://server.example.com/v1/auth/authorize?response_type=code&code_challenge_method=S256&code_challenge=mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs&authorization_details=%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22ldp_vc%22%2C%22locations%22%3A%5B%22https%3A%2F%2Ftest%2Ecom%22%2C%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%5D%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%7D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb&client_id=test-client&scope=openid', ); }); it('it should respond with insufficient_authorization when no sessions are provided', async () => { const url = new URL(`${MOCK_URL}/authorize-challenge`); const responseBody = { error: 'insufficient_authorization', auth_session: '123456789', presentation: '/authorize?client_id=..&request_uri=https://rp.example.com/oidc/request/1234', }; (await client.retrieveServerMetadata()).authorization_challenge_endpoint = url.toString(); nock(url.origin).post(url.pathname, { client_id: client.clientId }).times(1).reply(400, responseBody); await expect(client.acquireAuthorizationChallengeCode({ clientId: client.clientId })).rejects.toEqual({ error: 'insufficient_authorization', auth_session: '123456789', presentation: '/authorize?client_id=..&request_uri=https://rp.example.com/oidc/request/1234', }); }); it('it should successfully respond with a authorization code when authorization challenge is used', async () => { const url = new URL(`${MOCK_URL}/authorize-challenge`); const responseBody = { authorization_code: 'test_authorization_code', }; (await client.retrieveServerMetadata()).authorization_challenge_endpoint = url.toString(); const authSession = 'test-authSession'; const presentationDuringIssuanceSession = 'test-presentationDuringIssuanceSession'; nock(url.origin) .post(url.pathname, { client_id: client.clientId, auth_session: authSession, presentation_during_issuance_session: presentationDuringIssuanceSession, }) .times(1) .reply(200, responseBody); const response = await client.acquireAuthorizationChallengeCode({ clientId: client.clientId, authSession, presentationDuringIssuanceSession }); expect(response).toBeDefined(); expect(response.authorization_code).toEqual(responseBody.authorization_code); }); }); describe('should successfully handle isEbsi function', () => { it('should return true when calling isEbsi function', async () => { nock(MOCK_URL).get(/.*/).reply(200, {}); nock(MOCK_URL).get(WellKnownEndpoints.OAUTH_AS).reply(404, {}); nock(MOCK_URL).get(WellKnownEndpoints.OPENID_CONFIGURATION).reply(404, {}); const client = await OpenID4VCIClient.fromURI({ clientId: 'test-client', uri: 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fserver.example.com%22%2C%20%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22VerifiableAttestation%22%2C%22CTWalletSameAuthorisedInTime%22%5D%2C%22trust_framework%22%3A%7B%22name%22%3A%22ebsi%22%2C%22type%22%3A%22Accreditation%22%2C%22uri%22%3A%22TIR%20link%20towards%20accreditation%22%7D%7D%5D%7D', createAuthorizationRequestURL: false, }); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore client._state.endpointMetadata?.credentialIssuerMetadata = { credentials_supported: { TestCredential: { trust_framework: { name: 'ebsi_trust', }, }, }, }; expect(client.isEBSI()).toBe(true); }); }); it('determine to be version 13', async () => { const offer = { grants: { 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { 'pre-authorized_code': 'random', }, }, credential_configuration_ids: ['Omzetbelasting'], credential_issuer: 'https://example.com', } satisfies CredentialOfferPayloadV1_0_13; const offerUri = createCredentialOfferURIFromObject({ credential_offer: offer }, 'VALUE'); expect(determineSpecVersionFromOffer(offer)).toEqual(OpenId4VCIVersion.VER_1_0_13); expect(determineSpecVersionFromURI(offerUri)).toEqual(OpenId4VCIVersion.VER_1_0_13); }); it('determine to be version 11', async () => { const offerUri = 'openid-credential-offer://?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22wN39X8fU4FCU2MaykNRkCr%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22dbc2023%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fssi.dutchblockchaincoalition.org%2Fagent%22%7D'; expect(determineSpecVersionFromURI(offerUri)).toEqual(OpenId4VCIVersion.VER_1_0_11); });