diff --git a/packages/client/lib/AccessTokenClient.ts b/packages/client/lib/AccessTokenClient.ts index 63f38684..92e1b949 100644 --- a/packages/client/lib/AccessTokenClient.ts +++ b/packages/client/lib/AccessTokenClient.ts @@ -37,7 +37,9 @@ export class AccessTokenClient { const { request } = credentialOffer; const isPinRequired = this.isPinRequiredValue(request); - const issuerOpts = { issuer: getIssuerFromCredentialOfferPayload(request) }; + const issuerOpts = { + issuer: getIssuerFromCredentialOfferPayload(request) ? (getIssuerFromCredentialOfferPayload(request) as string) : (metadata?.issuer as string), + }; return await this.acquireAccessTokenUsingRequest({ accessTokenRequest: await this.createAccessTokenRequest({ diff --git a/packages/client/lib/CredentialRequestClientBuilderV1_0_09.ts b/packages/client/lib/CredentialRequestClientBuilderV1_0_09.ts index 754abecb..44013ff6 100644 --- a/packages/client/lib/CredentialRequestClientBuilderV1_0_09.ts +++ b/packages/client/lib/CredentialRequestClientBuilderV1_0_09.ts @@ -36,7 +36,9 @@ export class CredentialRequestClientBuilderV1_0_09 { metadata?: EndpointMetadata; }): CredentialRequestClientBuilderV1_0_09 { const builder = new CredentialRequestClientBuilderV1_0_09(); - const issuer = getIssuerFromCredentialOfferPayload(request); + const issuer = getIssuerFromCredentialOfferPayload(request) + ? (getIssuerFromCredentialOfferPayload(request) as string) + : (metadata?.issuer as string); builder.withCredentialEndpoint( metadata?.credential_endpoint ? metadata.credential_endpoint : issuer.endsWith('/') ? `${issuer}credential` : `${issuer}/credential` ); diff --git a/packages/client/lib/MetadataClient.ts b/packages/client/lib/MetadataClient.ts index d4501755..a358b29c 100644 --- a/packages/client/lib/MetadataClient.ts +++ b/packages/client/lib/MetadataClient.ts @@ -30,7 +30,10 @@ export class MetadataClient { * @param request */ public static async retrieveAllMetadataFromCredentialOfferRequest(request: CredentialOfferPayload): Promise { - return MetadataClient.retrieveAllMetadata(getIssuerFromCredentialOfferPayload(request)); + if (getIssuerFromCredentialOfferPayload(request)) { + return MetadataClient.retrieveAllMetadata(getIssuerFromCredentialOfferPayload(request) as string); + } + throw new Error("can't retrieve metadata from CredentialOfferRequest. No issuer field is present"); } /** diff --git a/packages/client/lib/__tests__/CredentialRequestClient.spec.ts b/packages/client/lib/__tests__/CredentialRequestClient.spec.ts index a2cbda99..becdfcc9 100644 --- a/packages/client/lib/__tests__/CredentialRequestClient.spec.ts +++ b/packages/client/lib/__tests__/CredentialRequestClient.spec.ts @@ -3,9 +3,11 @@ import { KeyObject } from 'crypto'; import { Alg, CredentialRequest, + EndpointMetadata, getIssuerFromCredentialOfferPayload, Jwt, ProofOfPossession, + ProofType, Typ, URL_NOT_VALID, WellKnownEndpoints, @@ -13,11 +15,11 @@ import { import * as jose from 'jose'; import nock from 'nock'; -import { CredentialRequestClientBuilderV1_0_09, MetadataClient } from '..'; -import { ProofOfPossessionBuilder } from '..'; +import { CredentialRequestClientBuilderV1_0_09, MetadataClient, ProofOfPossessionBuilder } from '..'; import { CredentialOffer } from '../CredentialOffer'; import { IDENTIPROOF_ISSUER_URL, IDENTIPROOF_OID4VCI_METADATA, INITIATION_TEST, WALT_OID4VCI_METADATA } from './MetadataMocks'; +import { getMockData } from './data/VciDataFixtures'; const partialJWT = 'eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmN'; @@ -149,7 +151,7 @@ describe('Credential Request Client with Walt.id ', () => { const credentialOffer = CredentialOffer.fromURI(WALT_IRR_URI); const request = credentialOffer.request; - const metadata = await MetadataClient.retrieveAllMetadata(getIssuerFromCredentialOfferPayload(request)); + const metadata = await MetadataClient.retrieveAllMetadata(getIssuerFromCredentialOfferPayload(request) as string); expect(metadata.credential_endpoint).toEqual(WALT_OID4VCI_METADATA.credential_endpoint); expect(metadata.token_endpoint).toEqual(WALT_OID4VCI_METADATA.token_endpoint); @@ -160,3 +162,100 @@ describe('Credential Request Client with Walt.id ', () => { expect(credReqClient.credentialRequestOpts.credentialEndpoint).toBe(WALT_OID4VCI_METADATA.credential_endpoint); }); }); + +describe('Credential Request Client with different issuers ', () => { + it('should create correct CredentialRequest for Spruce', async () => { + const IRR_URI = + 'openid-initiate-issuance://?issuer=https%3A%2F%2Fngi%2Doidc4vci%2Dtest%2Espruceid%2Exyz&credential_type=OpenBadgeCredential&pre-authorized_code=eyJhbGciOiJFUzI1NiJ9.eyJjcmVkZW50aWFsX3R5cGUiOlsiT3BlbkJhZGdlQ3JlZGVudGlhbCJdLCJleHAiOiIyMDIzLTA0LTIwVDA5OjA0OjM2WiIsIm5vbmNlIjoibWFibmVpT0VSZVB3V3BuRFFweEt3UnRsVVRFRlhGUEwifQ.qOZRPN8sTv_knhp7WaWte2-aDULaPZX--2i9unF6QDQNUllqDhvxgIHMDCYHCV8O2_Gj-T2x1J84fDMajE3asg&user_pin_required=false'; + const credentialOffer = await CredentialRequestClientBuilderV1_0_09.fromURI({ + uri: IRR_URI, + metadata: getMockData('spruce')?.metadata as unknown as EndpointMetadata, + }) + .build() + .createCredentialRequest({ + proofInput: { + proof_type: ProofType.JWT, + jwt: getMockData('spruce')?.credential.request.proof.jwt as string, + }, + credentialType: 'OpenBadgeCredential', + format: 'jwt_vc', + }); + expect(credentialOffer).toEqual(getMockData('spruce')?.credential.request); + }); + + it('should create correct CredentialRequest for Walt', async () => { + const IRR_URI = + 'openid-initiate-issuance://?issuer=https%3A%2F%2Fjff.walt.id%2Fissuer-api%2Fdefault%2Foidc%2F&credential_type=OpenBadgeCredential&pre-authorized_code=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIwMTc4OTNjYy04ZTY3LTQxNzItYWZlOS1lODcyYmYxNDBlNWMiLCJwcmUtYXV0aG9yaXplZCI6dHJ1ZX0.ODfq2AIhOcB61dAb3zMrXBJjPJaf53zkeHh_AssYyYA&user_pin_required=false'; + const credentialOffer = await CredentialRequestClientBuilderV1_0_09.fromURI({ + uri: IRR_URI, + metadata: getMockData('walt')?.metadata as unknown as EndpointMetadata, + }) + .build() + .createCredentialRequest({ + proofInput: { + proof_type: ProofType.JWT, + jwt: getMockData('walt')?.credential.request.proof.jwt as string, + }, + credentialType: 'OpenBadgeCredential', + format: 'jwt_vc', + }); + expect(credentialOffer).toEqual(getMockData('walt')?.credential.request); + }); + + it('should create correct CredentialRequest for uniissuer', async () => { + const IRR_URI = + 'https://oidc4vc.uniissuer.io/&credential_type=OpenBadgeCredential&pre-authorized_code=0ApoI8rxVmdQ44RIpuDbFIURIIkOhyek&user_pin_required=false'; + const credentialOffer = await CredentialRequestClientBuilderV1_0_09.fromURI({ + uri: IRR_URI, + metadata: getMockData('uniissuer')?.metadata as unknown as EndpointMetadata, + }) + .build() + .createCredentialRequest({ + proofInput: { + proof_type: ProofType.JWT, + jwt: getMockData('uniissuer')?.credential.request.proof.jwt as string, + }, + credentialType: 'OpenBadgeCredential', + format: 'jwt_vc', + }); + expect(credentialOffer).toEqual(getMockData('uniissuer')?.credential.request); + }); + + it('should create correct CredentialRequest for mattr', async () => { + const IRR_URI = + 'openid-initiate-issuance://?issuer=https://launchpad.mattrlabs.com&credential_type=OpenBadgeCredential&pre-authorized_code=g0UCOj6RAN5AwHU6gczm_GzB4_lH6GW39Z0Dl2DOOiO'; + const credentialOffer = await CredentialRequestClientBuilderV1_0_09.fromURI({ + uri: IRR_URI, + metadata: getMockData('mattr')?.metadata as unknown as EndpointMetadata, + }) + .build() + .createCredentialRequest({ + proofInput: { + proof_type: ProofType.JWT, + jwt: getMockData('mattr')?.credential.request.proof.jwt as string, + }, + credentialType: 'OpenBadgeCredential', + format: 'ldp_vc', + }); + expect(credentialOffer).toEqual(getMockData('mattr')?.credential.request); + }); + + it('should create correct CredentialRequest for diwala', async () => { + const IRR_URI = + 'openid-initiate-issuance://?issuer=https://oidc4vc.diwala.io&credential_type=OpenBadgeCredential&pre-authorized_code=eyJhbGciOiJIUzI1NiJ9.eyJjcmVkZW50aWFsX3R5cGUiOiJPcGVuQmFkZ2VDcmVkZW50aWFsIiwiZXhwIjoxNjgxOTg0NDY3fQ.fEAHKz2nuWfiYHw406iNxr-81pWkNkbi31bWsYSf6Ng'; + const credentialOffer = await CredentialRequestClientBuilderV1_0_09.fromURI({ + uri: IRR_URI, + metadata: getMockData('diwala')?.metadata as unknown as EndpointMetadata, + }) + .build() + .createCredentialRequest({ + proofInput: { + proof_type: ProofType.JWT, + jwt: getMockData('diwala')?.credential.request.proof.jwt as string, + }, + credentialType: 'OpenBadgeCredential', + format: 'ldp_vc', + }); + expect(credentialOffer).toEqual(getMockData('diwala')?.credential.request); + }); +}); diff --git a/packages/client/lib/__tests__/MetadataClient.spec.ts b/packages/client/lib/__tests__/MetadataClient.spec.ts index 73a5c8c5..b37e5d12 100644 --- a/packages/client/lib/__tests__/MetadataClient.spec.ts +++ b/packages/client/lib/__tests__/MetadataClient.spec.ts @@ -39,7 +39,7 @@ describe('MetadataClient with IdentiProof Issuer should', () => { const INITIATE_URI = 'openid-initiate-issuance://?issuer=https%3A%2F%2Fissuer.research.identiproof.io&credential_type=OpenBadgeCredential&pre-authorized_code=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhOTUyZjUxNi1jYWVmLTQ4YjMtODIxYy00OTRkYzgyNjljZjAiLCJwcmUtYXV0aG9yaXplZCI6dHJ1ZX0.YE5DlalcLC2ChGEg47CQDaN1gTxbaQqSclIVqsSAUHE&user_pin_required=false'; const initiation = CredentialOffer.fromURI(INITIATE_URI); - const metadata = await MetadataClient.retrieveAllMetadata(getIssuerFromCredentialOfferPayload(initiation.request)); + const metadata = await MetadataClient.retrieveAllMetadata(getIssuerFromCredentialOfferPayload(initiation.request) as string); expect(metadata.credential_endpoint).toEqual('https://issuer.research.identiproof.io/credential'); expect(metadata.token_endpoint).toEqual('https://auth.research.identiproof.io/oauth2/token'); expect(metadata.issuerMetadata).toEqual(IDENTIPROOF_OID4VCI_METADATA); diff --git a/packages/client/lib/__tests__/data/VciDataFixtures.ts b/packages/client/lib/__tests__/data/VciDataFixtures.ts index 9d282ad1..d3224906 100644 --- a/packages/client/lib/__tests__/data/VciDataFixtures.ts +++ b/packages/client/lib/__tests__/data/VciDataFixtures.ts @@ -1,15 +1,11 @@ -import { - CredentialSupportedBrief, - IssuerCredentialSubjectDisplay, - IssuerMetadataV1_0_08 -} from '@sphereon/openid4vci-common'; +import { CredentialSupportedBrief, IssuerCredentialSubjectDisplay, IssuerMetadataV1_0_08 } from '@sphereon/openid4vci-common'; import { ICredentialStatus, W3CVerifiableCredential } from '@sphereon/ssi-types'; -export function getMockData(issuerName: string): IssuerMockData|null { +export function getMockData(issuerName: string): IssuerMockData | null { if (issuerName in mockData) { return mockData[issuerName]; } - return null + return null; } export interface VciMockDataStructure { @@ -44,6 +40,7 @@ export interface IssuerMockData { }; credential: { url: string; + deeplink: string; request: { type: string; format: 'jwt_vc' | 'ldp_vc' | string; @@ -110,6 +107,8 @@ const mockData: VciMockDataStructure = { }, credential: { url: 'https://ngi-oidc4vci-test.spruceid.xyz/credential', + deeplink: + 'openid-initiate-issuance://?issuer=https%3A%2F%2Fngi%2Doidc4vci%2Dtest%2Espruceid%2Exyz&credential_type=OpenBadgeCredential&pre-authorized_code=eyJhbGciOiJFUzI1NiJ9.eyJjcmVkZW50aWFsX3R5cGUiOlsiT3BlbkJhZGdlQ3JlZGVudGlhbCJdLCJleHAiOiIyMDIzLTA0LTIwVDA5OjA0OjM2WiIsIm5vbmNlIjoibWFibmVpT0VSZVB3V3BuRFFweEt3UnRsVVRFRlhGUEwifQ.qOZRPN8sTv_knhp7WaWte2-aDULaPZX--2i9unF6QDQNUllqDhvxgIHMDCYHCV8O2_Gj-T2x1J84fDMajE3asg&user_pin_required=false', request: { type: 'OpenBadgeCredential', format: 'jwt_vc', @@ -353,6 +352,8 @@ const mockData: VciMockDataStructure = { }, }, credential: { + deeplink: + 'openid-initiate-issuance://?issuer=https%3A%2F%2Fjff.walt.id%2Fissuer-api%2Fdefault%2Foidc%2F&credential_type=OpenBadgeCredential&pre-authorized_code=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIwMTc4OTNjYy04ZTY3LTQxNzItYWZlOS1lODcyYmYxNDBlNWMiLCJwcmUtYXV0aG9yaXplZCI6dHJ1ZX0.ODfq2AIhOcB61dAb3zMrXBJjPJaf53zkeHh_AssYyYA&user_pin_required=false', url: 'https://jff.walt.id/issuer-api/default/oidc/credential', request: { type: 'OpenBadgeCredential', @@ -469,6 +470,8 @@ const mockData: VciMockDataStructure = { }, }, credential: { + deeplink: + 'https://oidc4vc.uniissuer.io/&credential_type=OpenBadgeCredential&pre-authorized_code=0ApoI8rxVmdQ44RIpuDbFIURIIkOhyek&user_pin_required=false', url: 'https://oidc4vc.uniissuer.io/1.0/credential', request: { type: 'OpenBadgeCredential', @@ -566,6 +569,8 @@ const mockData: VciMockDataStructure = { }, }, credential: { + deeplink: + 'openid-initiate-issuance://?issuer=https://launchpad.mattrlabs.com&credential_type=OpenBadgeCredential&pre-authorized_code=g0UCOj6RAN5AwHU6gczm_GzB4_lH6GW39Z0Dl2DOOiO', url: 'https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/credential', request: { type: 'OpenBadgeCredential', @@ -678,6 +683,8 @@ const mockData: VciMockDataStructure = { }, }, credential: { + deeplink: + 'openid-initiate-issuance://?issuer=https://oidc4vc.diwala.io&credential_type=OpenBadgeCredential&pre-authorized_code=eyJhbGciOiJIUzI1NiJ9.eyJjcmVkZW50aWFsX3R5cGUiOiJPcGVuQmFkZ2VDcmVkZW50aWFsIiwiZXhwIjoxNjgxOTg0NDY3fQ.fEAHKz2nuWfiYHw406iNxr-81pWkNkbi31bWsYSf6Ng', url: 'https://oidc4vc.diwala.io/credential', request: { type: 'OpenBadgeCredential', diff --git a/packages/common/lib/functions/CredentialOfferUtil.ts b/packages/common/lib/functions/CredentialOfferUtil.ts index 9bde52f8..6c916695 100644 --- a/packages/common/lib/functions/CredentialOfferUtil.ts +++ b/packages/common/lib/functions/CredentialOfferUtil.ts @@ -1,4 +1,4 @@ -import { CredentialOfferPayload, DefaultURISchemes, OpenId4VCIVersion, TokenErrorResponse } from '../types'; +import { CredentialOfferPayload, DefaultURISchemes, OpenId4VCIVersion } from '../types'; export function determineSpecVersionFromURI(uri: string): OpenId4VCIVersion { let version: OpenId4VCIVersion = OpenId4VCIVersion.VER_UNKNOWN; @@ -50,9 +50,9 @@ function recordVersion(determinedVersion: OpenId4VCIVersion, potentialVersion: O ); } -export function getIssuerFromCredentialOfferPayload(request: CredentialOfferPayload): string { +export function getIssuerFromCredentialOfferPayload(request: CredentialOfferPayload): string | undefined { if (!request || !('issuer' in request) || 'credential_issuer' in request) { - throw new Error(TokenErrorResponse.invalid_request); + return undefined; } return 'issuer' in request ? request.issuer : request['credential_issuer']; }