From 14e1deedd79d15ddba79d93e9d9c1abfa670c046 Mon Sep 17 00:00:00 2001 From: sander Date: Fri, 22 Dec 2023 20:39:06 +0100 Subject: [PATCH] Initial commit of getting OpenID4VCIClient without URI --- packages/client/lib/OpenID4VCIClient.ts | 64 +++++++--- .../lib/__tests__/OpenID4VCIClient.spec.ts | 112 +++++++++++++----- .../common/lib/types/Authorization.types.ts | 1 + 3 files changed, 135 insertions(+), 42 deletions(-) diff --git a/packages/client/lib/OpenID4VCIClient.ts b/packages/client/lib/OpenID4VCIClient.ts index 227eb835..76b7fe7b 100644 --- a/packages/client/lib/OpenID4VCIClient.ts +++ b/packages/client/lib/OpenID4VCIClient.ts @@ -15,7 +15,10 @@ import { PushedAuthorizationResponse, ResponseType, } from '@sphereon/oid4vci-common'; -import { getSupportedCredentials, getTypesFromCredentialSupported } from '@sphereon/oid4vci-common/dist/functions/IssuerMetadataUtils'; +import { + getSupportedCredentials, + getTypesFromCredentialSupported +} from '@sphereon/oid4vci-common/dist/functions/IssuerMetadataUtils'; import { CredentialSupportedTypeV1_0_08 } from '@sphereon/oid4vci-common/dist/types/v1_0_08.types'; import { CredentialFormat } from '@sphereon/ssi-types'; import Debug from 'debug'; @@ -46,28 +49,31 @@ interface AuthRequestOpts { } export class OpenID4VCIClient { - private readonly _credentialOffer: CredentialOfferRequestWithBaseUrl; + private readonly _credentialOffer: CredentialOfferRequestWithBaseUrl | undefined; private _clientId?: string; private _kid: string | undefined; private _alg: Alg | string | undefined; private _endpointMetadata: EndpointMetadataResult | undefined; private _accessTokenResponse: AccessTokenResponse | undefined; + private _issuer: string | undefined; - private constructor(credentialOffer: CredentialOfferRequestWithBaseUrl, kid?: string, alg?: Alg | string, clientId?: string) { + private constructor(credentialOffer?: CredentialOfferRequestWithBaseUrl, issuer?: string, kid?: string, alg?: Alg | string, clientId?: string) { this._credentialOffer = credentialOffer; + this._issuer = issuer; this._kid = kid; this._alg = alg; this._clientId = clientId; } + public static async fromURI({ - uri, - kid, - alg, - retrieveServerMetadata, - clientId, - resolveOfferUri, - }: { + uri, + kid, + alg, + retrieveServerMetadata, + clientId, + resolveOfferUri + }: { uri: string; kid?: string; alg?: Alg | string; @@ -75,7 +81,28 @@ export class OpenID4VCIClient { resolveOfferUri?: boolean; clientId?: string; }): Promise { - const client = new OpenID4VCIClient(await CredentialOfferClient.fromURI(uri, { resolve: resolveOfferUri }), kid, alg, clientId); + const client = new OpenID4VCIClient(await CredentialOfferClient.fromURI(uri, { resolve: resolveOfferUri }), undefined, kid, alg, clientId); + + if (retrieveServerMetadata === undefined || retrieveServerMetadata) { + await client.retrieveServerMetadata(); + } + return client; + } + + public static async fromIssuer({ + issuer, + kid, + alg, + retrieveServerMetadata, + clientId + }: { + issuer: string; + kid?: string; + alg?: Alg | string; + retrieveServerMetadata?: boolean; + clientId?: string; + }): Promise { + const client = new OpenID4VCIClient(undefined, issuer, kid, alg, clientId); if (retrieveServerMetadata === undefined || retrieveServerMetadata) { await client.retrieveServerMetadata(); @@ -86,7 +113,11 @@ export class OpenID4VCIClient { public async retrieveServerMetadata(): Promise { this.assertIssuerData(); if (!this._endpointMetadata) { - this._endpointMetadata = await MetadataClient.retrieveAllMetadataFromCredentialOffer(this.credentialOffer); + if (this._issuer) { + this._endpointMetadata = await MetadataClient.retrieveAllMetadata(this._issuer); + } else { + this._endpointMetadata = await MetadataClient.retrieveAllMetadataFromCredentialOffer(this.credentialOffer); + } } return this.endpointMetadata; } @@ -376,13 +407,13 @@ export class OpenID4VCIClient { // Then match the object array on server metadata getCredentialsSupported( restrictToInitiationTypes: boolean, - format?: (OID4VCICredentialFormat | string) | (OID4VCICredentialFormat | string)[], + format?: (OID4VCICredentialFormat | string) | (OID4VCICredentialFormat | string)[] ): CredentialSupported[] { return getSupportedCredentials({ issuerMetadata: this.endpointMetadata.credentialIssuerMetadata, version: this.version(), format: format, - types: restrictToInitiationTypes ? this.getCredentialOfferTypes() : undefined, + types: restrictToInitiationTypes ? this.getCredentialOfferTypes() : undefined }); } @@ -413,6 +444,9 @@ export class OpenID4VCIClient { } get credentialOffer(): CredentialOfferRequestWithBaseUrl { + if (!this._credentialOffer) { + throw new Error('no active credential offer available'); + } return this._credentialOffer; } @@ -473,7 +507,7 @@ export class OpenID4VCIClient { } private assertIssuerData(): void { - if (!this._credentialOffer) { + if (!this._credentialOffer && !this._issuer) { throw Error(`No issuance initiation or credential offer present`); } } diff --git a/packages/client/lib/__tests__/OpenID4VCIClient.spec.ts b/packages/client/lib/__tests__/OpenID4VCIClient.spec.ts index d29100bd..2ff1be9b 100644 --- a/packages/client/lib/__tests__/OpenID4VCIClient.spec.ts +++ b/packages/client/lib/__tests__/OpenID4VCIClient.spec.ts @@ -1,10 +1,12 @@ -import { CodeChallengeMethod, WellKnownEndpoints } from '@sphereon/oid4vci-common'; +import { CodeChallengeMethod, EndpointMetadataResult, Jwt, WellKnownEndpoints } from '@sphereon/oid4vci-common' // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore +import { SignJWT } from 'jose' import nock from 'nock'; import { OpenID4VCIClient } from '../OpenID4VCIClient'; + const MOCK_URL = 'https://server.example.com/'; describe('OpenID4VCIClient should', () => { @@ -95,25 +97,25 @@ describe('OpenID4VCIClient should', () => { format: 'ldp_vc', credential_definition: { '@context': ['https://www.w3.org/2018/credentials/v1', 'https://www.w3.org/2018/credentials/examples/v1'], - types: ['VerifiableCredential', 'UniversityDegreeCredential'], - }, + types: ['VerifiableCredential', 'UniversityDegreeCredential'] + } }, { type: 'openid_credential', format: 'mso_mdoc', - doctype: 'org.iso.18013.5.1.mDL', - }, + doctype: 'org.iso.18013.5.1.mDL' + } ], - redirectUri: 'http://localhost:8881/cb', - }), + redirectUri: 'http://localhost:8881/cb' + }) ).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%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%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%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%7D%5D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb&scope=openid&client_id=test-client', - ); - }); + '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%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%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%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%7D%5D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb&scope=openid&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._endpointMetadata?.credentialIssuerMetadata.authorization_endpoint = `${MOCK_URL}v1/auth/authorize`; + client._endpointMetadata?.credentialIssuerMetadata.authorization_endpoint = `${MOCK_URL}v1/auth/authorize` expect( client.createAuthorizationRequestUrl({ @@ -124,19 +126,19 @@ describe('OpenID4VCIClient should', () => { format: 'ldp_vc', credential_definition: { '@context': ['https://www.w3.org/2018/credentials/v1', 'https://www.w3.org/2018/credentials/examples/v1'], - types: ['VerifiableCredential', 'UniversityDegreeCredential'], - }, + types: ['VerifiableCredential', 'UniversityDegreeCredential'] + } }, - redirectUri: 'http://localhost:8881/cb', - }), + redirectUri: 'http://localhost:8881/cb' + }) ).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%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%7D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb&scope=openid&client_id=test-client', - ); - }); + '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%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%7D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb&scope=openid&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._endpointMetadata.credentialIssuerMetadata.authorization_endpoint = `${MOCK_URL}v1/auth/authorize`; + client._endpointMetadata.credentialIssuerMetadata.authorization_endpoint = `${MOCK_URL}v1/auth/authorize` expect( client.createAuthorizationRequestUrl({ @@ -148,14 +150,70 @@ describe('OpenID4VCIClient should', () => { 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'], - }, + types: ['VerifiableCredential', 'UniversityDegreeCredential'] + } }, scope: 'openid', - redirectUri: 'http://localhost:8881/cb', - }), + redirectUri: 'http://localhost:8881/cb' + }) ).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&scope=openid&client_id=test-client', - ); - }); -}); + '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&scope=openid&client_id=test-client' + ) + }) +}) + + +// Research code below +const ESIGNET_ISSUER_URL = 'https://esignet.collab.mosip.net' +describe('OpenID4VCIClient with authorizaion_code flow should', () => { + let client: OpenID4VCIClient + beforeEach(async () => { + + const origResponse = await fetch('https://esignet.collab.mosip.net/.well-known/openid-credential-issuer') + const responseText = await origResponse.text() + console.log(responseText) + + client = await OpenID4VCIClient.fromIssuer({ + issuer: ESIGNET_ISSUER_URL, + clientId: 'MUq1H5M4OBr9fxSC2fJrY4felRmxtDw4iRls2lBZQzI' + }) + }) + + let metaData: EndpointMetadataResult + it('retrieve server metadata', async () => { + metaData = await client.retrieveServerMetadata() + expect(metaData.token_endpoint).toBeDefined() + expect(metaData.issuer).toBeDefined() + expect(metaData.credential_endpoint).toBeDefined() + expect(metaData.credentialIssuerMetadata).toBeDefined() + expect(metaData.credentialIssuerMetadata.credentials_supported).toBeDefined() + expect(metaData.credentialIssuerMetadata.credentials_supported).toHaveLength(1) + console.log(metaData.credentialIssuerMetadata) + }) + + + async function proofOfPossessionCallbackFunction(args: Jwt, kid?: string): Promise { + return await new SignJWT({ ...args.payload }) + .setProtectedHeader({ ...args.header, kid: kid! }) + .setIssuer(kid!) + .setIssuedAt() + .setExpirationTime('2h') + .sign(importedJwk) + } + + it('get credential', async () => { + + const credentialsSupported = metaData.credentialIssuerMetadata.credentials_supported + const credentials = await client.acquireCredentials({ + credentialTypes: credentialsSupported[0], + format: 'ldp_vc', + proofCallbacks: { + signCallback: proofOfPossessionCallbackFunction + } + }) + + expect(credentials).toBeDefined() + expect(credentials.credential).toBeDefined() + }) + +}) \ No newline at end of file diff --git a/packages/common/lib/types/Authorization.types.ts b/packages/common/lib/types/Authorization.types.ts index 0276d510..9ebbdb99 100644 --- a/packages/common/lib/types/Authorization.types.ts +++ b/packages/common/lib/types/Authorization.types.ts @@ -156,6 +156,7 @@ export enum Encoding { export enum ResponseType { AUTH_CODE = 'code', + ID_TOKEN = 'id_token' } export enum CodeChallengeMethod {