From 968cc5ff4f829f528cc6b0941f69470a97c206ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zo=C3=AB=20Maas?= Date: Fri, 3 May 2024 15:25:55 +0200 Subject: [PATCH 1/5] chore: Added tests with credential_offer query params and http(s) schemes --- .../lib/__tests__/IssuanceInitiation.spec.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/client/lib/__tests__/IssuanceInitiation.spec.ts b/packages/client/lib/__tests__/IssuanceInitiation.spec.ts index f28e1f02..ddcfe7e3 100644 --- a/packages/client/lib/__tests__/IssuanceInitiation.spec.ts +++ b/packages/client/lib/__tests__/IssuanceInitiation.spec.ts @@ -58,4 +58,26 @@ describe('Issuance Initiation', () => { expect(client.credential_offer.credential_issuer).toEqual('https://launchpad.vii.electron.mattrlabs.io'); expect(client.preAuthorizedCode).toEqual('UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X'); }); + + it('Should take an https url as input and return a Credential Offer', async () => { + const client = await CredentialOfferClient.fromURI( + 'https://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Flaunchpad.vii.electron.mattrlabs.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22types%22%3A%5B%22OpenBadgeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X%22%7D%7D%7D', + ); + expect(client.version).toEqual(OpenId4VCIVersion.VER_1_0_11); + expect(client.baseUrl).toEqual('https://'); + expect(client.scheme).toEqual('https'); + expect(client.credential_offer.credential_issuer).toEqual('https://launchpad.vii.electron.mattrlabs.io'); + expect(client.preAuthorizedCode).toEqual('UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X'); + }) + + it('Should take an https url as input and return a Credential Offer', async () => { + const client = await CredentialOfferClient.fromURI( + 'http://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Flaunchpad.vii.electron.mattrlabs.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22types%22%3A%5B%22OpenBadgeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X%22%7D%7D%7D', + ); + expect(client.version).toEqual(OpenId4VCIVersion.VER_1_0_11); + expect(client.baseUrl).toEqual('http://'); + expect(client.scheme).toEqual('http'); + expect(client.credential_offer.credential_issuer).toEqual('https://launchpad.vii.electron.mattrlabs.io'); + expect(client.preAuthorizedCode).toEqual('UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X'); + }) }); From 4eaa40a765b91ee1237fd2ecc309d51821c9340c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zo=C3=AB=20Maas?= Date: Mon, 6 May 2024 14:11:45 +0200 Subject: [PATCH 2/5] chore: Added additional tests including http(s) urls --- packages/client/README.md | 20 +++++++ packages/client/lib/__tests__/IT.spec.ts | 27 ++++++++- .../lib/__tests__/IssuanceInitiation.spec.ts | 12 ++-- .../__tests__/CredentialOfferUtils.spec.ts | 56 +++++++++++++++++++ 4 files changed, 107 insertions(+), 8 deletions(-) diff --git a/packages/client/README.md b/packages/client/README.md index 2d4b5997..c828033d 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -51,6 +51,8 @@ client you can use to finish the pre-authorized code flows. This initiates the client using a URI obtained from the Issuer using a link (URL) or QR code typically. We are also already fetching the Server Metadata + +Using openid-initiate-issuance scheme ```typescript import { OpenID4VCIClient } from '@sphereon/oid4vci-client'; @@ -68,6 +70,24 @@ console.log(client.getCredentialEndpoint()); // https://issuer.research.identipr console.log(client.getAccessTokenEndpoint()); // https://auth.research.identiproof.io/oauth2/token ``` +Using https scheme +```typescript +import { OpenID4VCIClient } from '@sphereon/oid4vci-client'; + +// The client is initiated from a URI. This URI is provided by the Issuer, typically as a URL or QR code. +const client = await OpenID4VCIClient.fromURI({ + uri: 'https://launchpad.vii.electron.mattrlabs.io?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Flaunchpad.vii.electron.mattrlabs.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22types%22%3A%5B%22OpenBadgeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X%22%7D%7D%7D', + kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21#key-1', // Our DID. You can defer this also to when the acquireCredential method is called + alg: Alg.ES256, // The signing Algorithm we will use. You can defer this also to when the acquireCredential method is called + clientId: 'test-clientId', // The clientId if the Authrozation Service requires it. If a clientId is needed you can defer this also to when the acquireAccessToken method is called + retrieveServerMetadata: true, // Already retrieve the server metadata. Can also be done afterwards by invoking a method yourself. +}); + +console.log(client.getIssuer()); // https://launchpad.vii.electron.mattrlabs.io +console.log(client.getCredentialEndpoint()); // https://launchpad.vii.electron.mattrlabs.io/credential +console.log(client.getAccessTokenEndpoint()); // https://launchpad.vii.electron.mattrlabs.io/oauth2/token +``` + ## Server metadata The OID4VCI Server metadata contains information about token endpoints, credential endpoints, as well as additional diff --git a/packages/client/lib/__tests__/IT.spec.ts b/packages/client/lib/__tests__/IT.spec.ts index 00ca84ea..e94b8bc6 100644 --- a/packages/client/lib/__tests__/IT.spec.ts +++ b/packages/client/lib/__tests__/IT.spec.ts @@ -50,7 +50,8 @@ describe('OID4VCI-Client should', () => { const INITIATE_QR = 'openid-initiate-issuance://?issuer=https%3A%2F%2Fissuer.research.identiproof.io&credential_type=OpenBadgeCredentialUrl&pre-authorized_code=4jLs9xZHEfqcoow0kHE7d1a8hUk6Sy-5bVSV2MqBUGUgiFFQi-ImL62T-FmLIo8hKA1UdMPH0lM1xAgcFkJfxIw9L-lI3mVs0hRT8YVwsEM1ma6N3wzuCdwtMU4bcwKp&user_pin_required=true'; const OFFER_QR = - 'openid-credential-offer://credential_offer=%7B%22credential_issuer%22:%22https://credential-issuer.example.com%22,%22credentials%22:%5B%7B%22format%22:%22jwt_vc_json%22,%22types%22:%5B%22VerifiableCredential%22,%22UniversityDegreeCredential%22%5D%7D%5D,%22issuer_state%22:%22eyJhbGciOiJSU0Et...FYUaBy%22%7D'; + 'openid-credential-offer://credential_offer=%7B%22credential_issuer%22:%22https://issuer.research.identiproof.io%22,%22credentials%22:%5B%7B%22format%22:%22jwt_vc_json%22,%22types%22:%5B%22VerifiableCredential%22,%22UniversityDegreeCredential%22%5D%7D%5D,%22issuer_state%22:%22eyJhbGciOiJSU0Et...FYUaBy%22%7D'; + const HTTPS = 'https://issuer.research.identiproof.io?issuer=https%3A%2F%2Fissuer.research.identiproof.io&credential_type=OpenBadgeCredentialUrl&pre-authorized_code=4jLs9xZHEfqcoow0kHE7d1a8hUk6Sy-5bVSV2MqBUGUgiFFQi-ImL62T-FmLIo8hKA1UdMPH0lM1xAgcFkJfxIw9L-lI3mVs0hRT8YVwsEM1ma6N3wzuCdwtMU4bcwKp&user_pin_required=true' function succeedWithAFullFlowWithClientSetup() { nock(IDENTIPROOF_ISSUER_URL).get('/.well-known/openid-credential-issuer').reply(200, JSON.stringify(IDENTIPROOF_OID4VCI_METADATA)); @@ -78,7 +79,7 @@ describe('OID4VCI-Client should', () => { await assertionOfsucceedWithAFullFlowWithClient(client); }); - test.skip('succeed with a full flow wit the client using OpenID4VCI version 11', async () => { + test.skip('succeed with a full flow with the client using OpenID4VCI version 11 and deeplink', async () => { succeedWithAFullFlowWithClientSetup(); const client = await OpenID4VCIClient.fromURI({ uri: OFFER_QR, @@ -89,6 +90,28 @@ describe('OID4VCI-Client should', () => { await assertionOfsucceedWithAFullFlowWithClient(client); }); + /* +openid-initiate-issuance://?issuer=https://issuer.research.identiproof.io&credential_type=OpenBadgeCredentialUrl&pre-authorized_code=4jLs9xZHEfqcoow0kHE7d1a8hUk6Sy-5bVSV2MqBUGUgiFFQi-ImL62T-FmLIo8hKA1UdMPH0lM1xAgcFkJfxIw9L-lI3mVs0hRT8YVwsEM1ma6N3wzuCdwtMU4bcwKp&user_pin_required=true + +#################################################################################################################################### + +openid-credential-offer://credential_offer={"credential_issuer":"https://issuer.research.identiproof.io","credentials":[{"format":"jwt_vc_json","types":["VerifiableCredential","UniversityDegreeCredential"]}],"issuer_state":"eyJhbGciOiJSU0Et...FYUaBy"} + +#################################################################################################################################### + +https://issuer.research.identiproof.io?issuer=https://issuer.research.identiproof.io&credential_type=OpenBadgeCredentialUrl&pre-authorized_code=4jLs9xZHEfqcoow0kHE7d1a8hUk6Sy-5bVSV2MqBUGUgiFFQi-ImL62T-FmLIo8hKA1UdMPH0lM1xAgcFkJfxIw9L-lI3mVs0hRT8YVwsEM1ma6N3wzuCdwtMU4bcwKp&user_pin_required=true + */ + it('succeed with a full flow with the client using OpenID4VCI version 11 and https', async () => { + succeedWithAFullFlowWithClientSetup() + const client = await OpenID4VCIClient.fromURI({ + uri: HTTPS, + kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1', + alg: Alg.ES256, + clientId: 'test-clientId' + }) + await assertionOfsucceedWithAFullFlowWithClient(client); + }) + async function assertionOfsucceedWithAFullFlowWithClient(client: OpenID4VCIClient) { expect(client.credentialOffer).toBeDefined(); expect(client.endpointMetadata).toBeDefined(); diff --git a/packages/client/lib/__tests__/IssuanceInitiation.spec.ts b/packages/client/lib/__tests__/IssuanceInitiation.spec.ts index ddcfe7e3..1c6635a5 100644 --- a/packages/client/lib/__tests__/IssuanceInitiation.spec.ts +++ b/packages/client/lib/__tests__/IssuanceInitiation.spec.ts @@ -61,23 +61,23 @@ describe('Issuance Initiation', () => { it('Should take an https url as input and return a Credential Offer', async () => { const client = await CredentialOfferClient.fromURI( - 'https://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Flaunchpad.vii.electron.mattrlabs.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22types%22%3A%5B%22OpenBadgeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X%22%7D%7D%7D', + 'https://launchpad.vii.electron.mattrlabs.io?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Flaunchpad.vii.electron.mattrlabs.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22types%22%3A%5B%22OpenBadgeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X%22%7D%7D%7D', ); expect(client.version).toEqual(OpenId4VCIVersion.VER_1_0_11); - expect(client.baseUrl).toEqual('https://'); + expect(client.baseUrl).toEqual('https://launchpad.vii.electron.mattrlabs.io'); expect(client.scheme).toEqual('https'); expect(client.credential_offer.credential_issuer).toEqual('https://launchpad.vii.electron.mattrlabs.io'); expect(client.preAuthorizedCode).toEqual('UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X'); }) - it('Should take an https url as input and return a Credential Offer', async () => { + it('Should take an http url as input and return a Credential Offer', async () => { const client = await CredentialOfferClient.fromURI( - 'http://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Flaunchpad.vii.electron.mattrlabs.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22types%22%3A%5B%22OpenBadgeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X%22%7D%7D%7D', + 'http://launchpad.vii.electron.mattrlabs.io?credential_offer=%7B%22credential_issuer%22%3A%22http%3A%2F%2Flaunchpad.vii.electron.mattrlabs.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22types%22%3A%5B%22OpenBadgeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X%22%7D%7D%7D', ); expect(client.version).toEqual(OpenId4VCIVersion.VER_1_0_11); - expect(client.baseUrl).toEqual('http://'); + expect(client.baseUrl).toEqual('http://launchpad.vii.electron.mattrlabs.io'); expect(client.scheme).toEqual('http'); - expect(client.credential_offer.credential_issuer).toEqual('https://launchpad.vii.electron.mattrlabs.io'); + expect(client.credential_offer.credential_issuer).toEqual('http://launchpad.vii.electron.mattrlabs.io'); expect(client.preAuthorizedCode).toEqual('UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X'); }) }); diff --git a/packages/issuer/lib/__tests__/CredentialOfferUtils.spec.ts b/packages/issuer/lib/__tests__/CredentialOfferUtils.spec.ts index ed5b4fe0..b4a741b7 100644 --- a/packages/issuer/lib/__tests__/CredentialOfferUtils.spec.ts +++ b/packages/issuer/lib/__tests__/CredentialOfferUtils.spec.ts @@ -24,4 +24,60 @@ describe('CredentialOfferUtils should', () => { 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fcredential-issuer.example.com%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22eyJhbGciOiJSU0Et...FYUaBy%22%7D%7D%7D', ) }) + + it('create an https link from credentialOffer object', () => { + // below is the example from spec (https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-11.html#name-sending-credential-offer-by) and is wrong, the issuer_state should be in the grants and not a top-level property + // https://credential-issuer.example.com?credential_offer=%7B%22credential_issuer%22:%22https://credential-issuer.example.com%22,%22credentials%22:%5B%7B%22format%22:%22jwt_vc_json%22,%22types%22:%5B%22VerifiableCredential%22,%22UniversityDegreeCredential%22%5D%7D%5D,%22issuer_state%22:%22eyJhbGciOiJSU0Et...FYUaBy%22%7D + const credentialOffer = { + credential_issuer: 'https://credential-issuer.example.com', + credentials: [ + { + format: 'jwt_vc_json', + types: ['VerifiableCredential', 'UniversityDegreeCredential'], + }, + ], + grants: { + authorization_code: { + issuer_state: 'eyJhbGciOiJSU0Et...FYUaBy', + }, + }, + } as CredentialOfferPayloadV1_0_11 + + expect(createCredentialOfferURI({ + credential_issuer: credentialOffer.credential_issuer, + credential_endpoint: 'test_issuer', + issuer: 'test_issuer', + credentials_supported: [] + }, { credentialOffer, state: 'eyJhbGciOiJSU0Et...FYUaBy', scheme: 'https' })).toEqual( + `${credentialOffer.credential_issuer}?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fcredential-issuer.example.com%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22eyJhbGciOiJSU0Et...FYUaBy%22%7D%7D%7D`, + ) + }) + + it('create an http link from credentialOffer object', () => { + // below is the example from spec (https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-11.html#name-sending-credential-offer-by) and is wrong, the issuer_state should be in the grants and not a top-level property + // http://credential-issuer.example.com?credential_offer=%7B%22credential_issuer%22:%22http://credential-issuer.example.com%22,%22credentials%22:%5B%7B%22format%22:%22jwt_vc_json%22,%22types%22:%5B%22VerifiableCredential%22,%22UniversityDegreeCredential%22%5D%7D%5D,%22issuer_state%22:%22eyJhbGciOiJSU0Et...FYUaBy%22%7D + const credentialOffer = { + credential_issuer: 'http://credential-issuer.example.com', + credentials: [ + { + format: 'jwt_vc_json', + types: ['VerifiableCredential', 'UniversityDegreeCredential'], + }, + ], + grants: { + authorization_code: { + issuer_state: 'eyJhbGciOiJSU0Et...FYUaBy', + }, + }, + } as CredentialOfferPayloadV1_0_11 + + expect(createCredentialOfferURI({ + credential_issuer: credentialOffer.credential_issuer, + credential_endpoint: 'test_issuer', + issuer: 'test_issuer', + credentials_supported: [] + }, { credentialOffer, state: 'eyJhbGciOiJSU0Et...FYUaBy', scheme: 'http' })).toEqual( + `${credentialOffer.credential_issuer}?credential_offer=%7B%22credential_issuer%22%3A%22http%3A%2F%2Fcredential-issuer.example.com%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22eyJhbGciOiJSU0Et...FYUaBy%22%7D%7D%7D`, + ) + }) }) From 578ee2c3ce5a8bfff68f624905f6bd0d8becfd48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zo=C3=AB=20Maas?= Date: Tue, 7 May 2024 13:35:11 +0200 Subject: [PATCH 3/5] chore: Implemented credential_offer decoding and added full flow tests --- packages/client/lib/CredentialOfferClient.ts | 8 ++-- packages/client/lib/__tests__/IT.spec.ts | 44 +++++++++++++------- packages/common/lib/functions/Encoding.ts | 37 ++++++++++++++-- 3 files changed, 65 insertions(+), 24 deletions(-) diff --git a/packages/client/lib/CredentialOfferClient.ts b/packages/client/lib/CredentialOfferClient.ts index 2318a5e8..c794a732 100644 --- a/packages/client/lib/CredentialOfferClient.ts +++ b/packages/client/lib/CredentialOfferClient.ts @@ -27,22 +27,20 @@ export class CredentialOfferClient { const version = determineSpecVersionFromURI(uri); let credentialOffer: CredentialOffer; let credentialOfferPayload: CredentialOfferPayload; + // credential offer was introduced in draft 9 and credential_offer_uri in draft 11 if (version < OpenId4VCIVersion.VER_1_0_11) { credentialOfferPayload = convertURIToJsonObject(uri, { arrayTypeProperties: ['credential_type'], - requiredProperties: uri.includes('credential_offer_uri=') ? ['credential_offer_uri'] : ['issuer', 'credential_type'], + requiredProperties: uri.includes('credential_offer=') ? ['credential_offer'] : ['issuer', 'credential_type'], }) as CredentialOfferPayloadV1_0_09; credentialOffer = { credential_offer: credentialOfferPayload, }; } else { credentialOffer = convertURIToJsonObject(uri, { - arrayTypeProperties: ['credentials'], + arrayTypeProperties: uri.includes('credential_offer_uri=') ? ['credential_offer_uri'] : ['credential_offer'], requiredProperties: uri.includes('credential_offer_uri=') ? ['credential_offer_uri'] : ['credential_offer'], }) as CredentialOfferV1_0_11; - if (credentialOffer?.credential_offer_uri === undefined && !credentialOffer?.credential_offer) { - throw Error('Either a credential_offer or credential_offer_uri should be present in ' + uri); - } } const request = await toUniformCredentialOfferRequest(credentialOffer, { diff --git a/packages/client/lib/__tests__/IT.spec.ts b/packages/client/lib/__tests__/IT.spec.ts index e94b8bc6..ee1757c4 100644 --- a/packages/client/lib/__tests__/IT.spec.ts +++ b/packages/client/lib/__tests__/IT.spec.ts @@ -50,8 +50,11 @@ describe('OID4VCI-Client should', () => { const INITIATE_QR = 'openid-initiate-issuance://?issuer=https%3A%2F%2Fissuer.research.identiproof.io&credential_type=OpenBadgeCredentialUrl&pre-authorized_code=4jLs9xZHEfqcoow0kHE7d1a8hUk6Sy-5bVSV2MqBUGUgiFFQi-ImL62T-FmLIo8hKA1UdMPH0lM1xAgcFkJfxIw9L-lI3mVs0hRT8YVwsEM1ma6N3wzuCdwtMU4bcwKp&user_pin_required=true'; const OFFER_QR = - 'openid-credential-offer://credential_offer=%7B%22credential_issuer%22:%22https://issuer.research.identiproof.io%22,%22credentials%22:%5B%7B%22format%22:%22jwt_vc_json%22,%22types%22:%5B%22VerifiableCredential%22,%22UniversityDegreeCredential%22%5D%7D%5D,%22issuer_state%22:%22eyJhbGciOiJSU0Et...FYUaBy%22%7D'; - const HTTPS = 'https://issuer.research.identiproof.io?issuer=https%3A%2F%2Fissuer.research.identiproof.io&credential_type=OpenBadgeCredentialUrl&pre-authorized_code=4jLs9xZHEfqcoow0kHE7d1a8hUk6Sy-5bVSV2MqBUGUgiFFQi-ImL62T-FmLIo8hKA1UdMPH0lM1xAgcFkJfxIw9L-lI3mVs0hRT8YVwsEM1ma6N3wzuCdwtMU4bcwKp&user_pin_required=true' + 'openid-credential-offer://?credential_offer%3D%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.research.identiproof.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22adhjhdjajkdkhjhdj%22%2C%22user_pin_required%22%3Atrue%7D%7D%7D'; + const HTTPS_INITIATE_QR = 'https://issuer.research.identiproof.io?issuer=https%3A%2F%2Fissuer.research.identiproof.io&credential_type=OpenBadgeCredentialUrl&pre-authorized_code=4jLs9xZHEfqcoow0kHE7d1a8hUk6Sy-5bVSV2MqBUGUgiFFQi-ImL62T-FmLIo8hKA1UdMPH0lM1xAgcFkJfxIw9L-lI3mVs0hRT8YVwsEM1ma6N3wzuCdwtMU4bcwKp&user_pin_required=true' + const HTTPS_OFFER_QR_AUTHORIZATION_CODE = + 'https://issuer.research.identiproof.io?credential_offer%3D%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.research.identiproof.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22eyJhbGciOiJSU0Et...FYUaBy%22%7D%7D%7D'; + const HTTPS_OFFER_QR_PRE_AUTHORIZED = 'https://issuer.research.identiproof.io?credential_offer%3D%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.research.identiproof.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22adhjhdjajkdkhjhdj%22%2C%22user_pin_required%22%3Atrue%7D%7D%7D' function succeedWithAFullFlowWithClientSetup() { nock(IDENTIPROOF_ISSUER_URL).get('/.well-known/openid-credential-issuer').reply(200, JSON.stringify(IDENTIPROOF_OID4VCI_METADATA)); @@ -79,7 +82,7 @@ describe('OID4VCI-Client should', () => { await assertionOfsucceedWithAFullFlowWithClient(client); }); - test.skip('succeed with a full flow with the client using OpenID4VCI version 11 and deeplink', async () => { + it('succeed with a full flow with the client using OpenID4VCI version 11 and deeplink', async () => { succeedWithAFullFlowWithClientSetup(); const client = await OpenID4VCIClient.fromURI({ uri: OFFER_QR, @@ -90,21 +93,32 @@ describe('OID4VCI-Client should', () => { await assertionOfsucceedWithAFullFlowWithClient(client); }); - /* -openid-initiate-issuance://?issuer=https://issuer.research.identiproof.io&credential_type=OpenBadgeCredentialUrl&pre-authorized_code=4jLs9xZHEfqcoow0kHE7d1a8hUk6Sy-5bVSV2MqBUGUgiFFQi-ImL62T-FmLIo8hKA1UdMPH0lM1xAgcFkJfxIw9L-lI3mVs0hRT8YVwsEM1ma6N3wzuCdwtMU4bcwKp&user_pin_required=true - -#################################################################################################################################### - -openid-credential-offer://credential_offer={"credential_issuer":"https://issuer.research.identiproof.io","credentials":[{"format":"jwt_vc_json","types":["VerifiableCredential","UniversityDegreeCredential"]}],"issuer_state":"eyJhbGciOiJSU0Et...FYUaBy"} + it('succeed with a full flow with the client using OpenID4VCI draft < 9 and https', async () => { + succeedWithAFullFlowWithClientSetup() + const client = await OpenID4VCIClient.fromURI({ + uri: HTTPS_INITIATE_QR, + kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1', + alg: Alg.ES256, + clientId: 'test-clientId' + }) + await assertionOfsucceedWithAFullFlowWithClient(client); + }) -#################################################################################################################################### + it('should succeed with a full flow with the client using OpenID4VCI draft > 11, https and authorization_code flow', async () => { + succeedWithAFullFlowWithClientSetup() + const client = await OpenID4VCIClient.fromURI({ + uri: HTTPS_OFFER_QR_AUTHORIZATION_CODE, + kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1', + alg: Alg.ES256, + clientId: 'test-clientId' + }) + await assertionOfsucceedWithAFullFlowWithClient(client); + }) -https://issuer.research.identiproof.io?issuer=https://issuer.research.identiproof.io&credential_type=OpenBadgeCredentialUrl&pre-authorized_code=4jLs9xZHEfqcoow0kHE7d1a8hUk6Sy-5bVSV2MqBUGUgiFFQi-ImL62T-FmLIo8hKA1UdMPH0lM1xAgcFkJfxIw9L-lI3mVs0hRT8YVwsEM1ma6N3wzuCdwtMU4bcwKp&user_pin_required=true - */ - it('succeed with a full flow with the client using OpenID4VCI version 11 and https', async () => { + it('should succeed with a full flow with the client using OpenID4VCI draft > 11, https and preauthorized_code flow', async () => { succeedWithAFullFlowWithClientSetup() const client = await OpenID4VCIClient.fromURI({ - uri: HTTPS, + uri: HTTPS_OFFER_QR_PRE_AUTHORIZED, kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1', alg: Alg.ES256, clientId: 'test-clientId' @@ -119,7 +133,7 @@ https://issuer.research.identiproof.io?issuer=https://issuer.research.identiproo expect(client.getCredentialEndpoint()).toEqual('https://issuer.research.identiproof.io/credential'); expect(client.getAccessTokenEndpoint()).toEqual('https://auth.research.identiproof.io/oauth2/token'); - const accessToken = await client.acquireAccessToken({ pin: '1234' }); + const accessToken = await client.acquireAccessToken({ pin: '1234', code: 'ABCD' }); expect(accessToken).toEqual(mockedAccessTokenResponse); const credentialResponse = await client.acquireCredentials({ diff --git a/packages/common/lib/functions/Encoding.ts b/packages/common/lib/functions/Encoding.ts index 80c4a960..b489bc5c 100644 --- a/packages/common/lib/functions/Encoding.ts +++ b/packages/common/lib/functions/Encoding.ts @@ -81,17 +81,46 @@ export function convertJsonToURI( } /** - * @function decodeUriAsJson decodes a URI into a Json object + * @type {(uri: string, opts?: DecodeURIAsJsonOpts): unknown} convertURIToJsonObject converts an URI into a Json object decoding its properties * @param uri string - * @param opts: + * @param opts * - requiredProperties: the required properties * - arrayTypeProperties: properties that can show up more that once + * @returns JSON object */ export function convertURIToJsonObject(uri: string, opts?: DecodeURIAsJsonOpts): unknown { if (!uri || (opts?.requiredProperties && !opts.requiredProperties?.every((p) => uri.includes(p)))) { throw new Error(BAD_PARAMS); } + + if (opts?.arrayTypeProperties && opts.arrayTypeProperties.includes('credential_offer_uri')) { + const value = uri.includes('?') ? uri.split('?')[1].split('=') : uri + if (!Array.isArray(value) || value[0] !== 'credential_offer_uri') { + throw new Error(`URL does not include credential_offer_uri`) + } + return { + credential_offer_uri: value[1] + } + } + + if (opts?.arrayTypeProperties && opts.arrayTypeProperties.includes('credential_offer')) { + const decodedUri = decodeURIComponent(uri) + const value = decodedUri.includes('?') ? decodedUri.split('?')[1].split('=') : decodedUri + if (!Array.isArray(value) || value[0] !== 'credential_offer') { + throw new Error(`URL does not include credential_offer`) + } + try { + const json = JSON.parse(value[1]) + return { + credential_offer: json + } + } catch(e) { + console.log(e) + } + } + const uriComponents = getURIComponentsAsArray(uri, opts?.arrayTypeProperties); + return decodeJsonProperties(uriComponents); } @@ -128,8 +157,8 @@ function decodeJsonProperties(parts: string[] | string[][]): unknown { /** * @function get URI Components as Array - * @param uri string - * @param arrayTypes array of string containing array like keys + * @param {string} uri uri + * @param {string[]} [arrayTypes] array of string containing array like keys */ function getURIComponentsAsArray(uri: string, arrayTypes?: string[]): string[] | string[][] { const parts = uri.includes('?') ? uri.split('?')[1] : uri.includes('://') ? uri.split('://')[1] : uri; From 71dbeac25c5497ee13c90373ecc3cad61e14829e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zo=C3=AB=20Maas?= Date: Tue, 7 May 2024 13:41:46 +0200 Subject: [PATCH 4/5] refactor: Refactored JSDoc --- packages/common/lib/functions/Encoding.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/common/lib/functions/Encoding.ts b/packages/common/lib/functions/Encoding.ts index b489bc5c..62089c15 100644 --- a/packages/common/lib/functions/Encoding.ts +++ b/packages/common/lib/functions/Encoding.ts @@ -1,9 +1,9 @@ import { BAD_PARAMS, DecodeURIAsJsonOpts, EncodeJsonAsURIOpts, JsonURIMode, OpenId4VCIVersion, SearchValue } from '../types'; /** - * @function encodeJsonAsURI encodes a Json object into a URI - * @param json object - * @param opts: + * @type {(json: {[s:string]: never} | ArrayLike | string | object, opts?: EncodeJsonAsURIOpts)} encodes a Json object into a URI + * @param { {[s:string]: never} | ArrayLike | string | object } json + * @param {EncodeJsonAsURIOpts} [opts] Option to encode json as uri * - urlTypeProperties: a list of properties of which the value is a URL * - arrayTypeProperties: a list of properties which are an array */ @@ -82,8 +82,8 @@ export function convertJsonToURI( /** * @type {(uri: string, opts?: DecodeURIAsJsonOpts): unknown} convertURIToJsonObject converts an URI into a Json object decoding its properties - * @param uri string - * @param opts + * @param {string} uri + * @param {DecodeURIAsJsonOpts} [opts] * - requiredProperties: the required properties * - arrayTypeProperties: properties that can show up more that once * @returns JSON object From f6c4d319a04cc9bcdf513c34f2141abdb76e281a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zo=C3=AB=20Maas?= Date: Tue, 7 May 2024 14:02:39 +0200 Subject: [PATCH 5/5] refactor: Refactored conversion from uri to draft 11 json --- packages/common/lib/functions/Encoding.ts | 50 +++++++++++++---------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/packages/common/lib/functions/Encoding.ts b/packages/common/lib/functions/Encoding.ts index 62089c15..355fdbe3 100644 --- a/packages/common/lib/functions/Encoding.ts +++ b/packages/common/lib/functions/Encoding.ts @@ -1,4 +1,12 @@ -import { BAD_PARAMS, DecodeURIAsJsonOpts, EncodeJsonAsURIOpts, JsonURIMode, OpenId4VCIVersion, SearchValue } from '../types'; +import { + BAD_PARAMS, + CredentialOfferV1_0_11, + DecodeURIAsJsonOpts, + EncodeJsonAsURIOpts, + JsonURIMode, + OpenId4VCIVersion, + SearchValue +} from '../types' /** * @type {(json: {[s:string]: never} | ArrayLike | string | object, opts?: EncodeJsonAsURIOpts)} encodes a Json object into a URI @@ -94,36 +102,34 @@ export function convertURIToJsonObject(uri: string, opts?: DecodeURIAsJsonOpts): } if (opts?.arrayTypeProperties && opts.arrayTypeProperties.includes('credential_offer_uri')) { - const value = uri.includes('?') ? uri.split('?')[1].split('=') : uri - if (!Array.isArray(value) || value[0] !== 'credential_offer_uri') { - throw new Error(`URL does not include credential_offer_uri`) - } - return { - credential_offer_uri: value[1] - } + return encodedCredentialOfferUri2Json({ uri }) } if (opts?.arrayTypeProperties && opts.arrayTypeProperties.includes('credential_offer')) { - const decodedUri = decodeURIComponent(uri) - const value = decodedUri.includes('?') ? decodedUri.split('?')[1].split('=') : decodedUri - if (!Array.isArray(value) || value[0] !== 'credential_offer') { - throw new Error(`URL does not include credential_offer`) - } - try { - const json = JSON.parse(value[1]) - return { - credential_offer: json - } - } catch(e) { - console.log(e) - } + return encodedCredentialOffer2Json({ uri }) } const uriComponents = getURIComponentsAsArray(uri, opts?.arrayTypeProperties); - return decodeJsonProperties(uriComponents); } +function encodedCredentialOfferUri2Json(args: { uri: string }): Pick { + const { uri } = args + const value = uri.includes('?') ? uri.split('?')[1].split('=') : uri + return { + credential_offer_uri: value[1] + } +} + +function encodedCredentialOffer2Json(args: { uri: string }): Pick { + const { uri } = args + const decodedUri = decodeURIComponent(uri) + const value = decodedUri.includes('?') ? decodedUri.split('?')[1].split('=') : decodedUri + return { + credential_offer: JSON.parse(value[1]) + } +} + function decodeJsonProperties(parts: string[] | string[][]): unknown { const json: { [s: string]: unknown } | ArrayLike = {}; for (const key in parts) {