@sphereon/oid4vci-client
Version:
OpenID for Verifiable Credential Issuance (OpenID4VCI) client
281 lines (255 loc) • 13.8 kB
text/typescript
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);
});