From 7304c066d80d845f852fbbdf759f9f3dbf7146bc Mon Sep 17 00:00:00 2001 From: sander Date: Wed, 10 Jan 2024 12:58:26 +0100 Subject: [PATCH] Minimal changes for OpenID V12 spec --- .../callback-example/lib/IssuerCallback.ts | 4 +- .../lib/__tests__/issuerCallback.spec.ts | 8 +- packages/client/lib/AccessTokenClient.ts | 2 +- packages/client/lib/CredentialOfferClient.ts | 30 ++-- .../client/lib/CredentialRequestClient.ts | 4 +- .../lib/CredentialRequestClientBuilder.ts | 12 +- packages/client/lib/MetadataClient.ts | 133 ++++++++++++------ packages/client/lib/OpenID4VCIClient.ts | 46 +++--- .../client/lib/ProofOfPossessionBuilder.ts | 8 +- packages/client/lib/__tests__/IT.spec.ts | 2 +- .../lib/__tests__/IssuanceInitiation.spec.ts | 2 +- .../ProofOfPossessionBuilder.spec.ts | 2 +- .../lib/__tests__/CredentialOfferUtil.spec.ts | 4 +- .../lib/functions/CredentialOfferUtil.ts | 120 +++------------- .../lib/functions/CredentialRequestUtil.ts | 17 +-- packages/common/lib/functions/Encoding.ts | 4 +- packages/common/lib/functions/FormatUtils.ts | 12 +- .../lib/functions/IssuerMetadataUtils.ts | 54 ++----- .../common/lib/types/Authorization.types.ts | 1 + packages/common/lib/types/Generic.types.ts | 15 +- .../lib/types/OpenID4VCIVersions.types.ts | 5 +- packages/common/lib/types/ServerMetadata.ts | 5 +- packages/common/lib/types/index.ts | 4 +- packages/common/lib/types/v1_0_08.types.ts | 49 ------- packages/common/lib/types/v1_0_09.types.ts | 36 ----- packages/issuer-rest/lib/OID4VCIServer.ts | 8 +- .../lib/__tests__/ClientIssuerIT.spec.ts | 6 +- .../issuer-rest/lib/oid4vci-api-functions.ts | 4 +- packages/issuer/lib/VcIssuer.ts | 19 ++- .../__tests__/CredentialOfferUtils.spec.ts | 4 +- .../issuer/lib/__tests__/VcIssuer.spec.ts | 6 +- .../lib/__tests__/VcIssuerBuilder.spec.ts | 16 +-- ....ts => CredentialSupportedBuilderV1_12.ts} | 34 +++-- .../lib/builder/IssuerMetadataBuilderV1_11.ts | 10 +- .../issuer/lib/builder/VcIssuerBuilder.ts | 4 +- packages/issuer/lib/builder/index.ts | 2 +- .../lib/functions/CredentialOfferUtils.ts | 14 +- 37 files changed, 259 insertions(+), 447 deletions(-) delete mode 100644 packages/common/lib/types/v1_0_08.types.ts delete mode 100644 packages/common/lib/types/v1_0_09.types.ts rename packages/issuer/lib/builder/{CredentialSupportedBuilderV1_11.ts => CredentialSupportedBuilderV1_12.ts} (89%) diff --git a/packages/callback-example/lib/IssuerCallback.ts b/packages/callback-example/lib/IssuerCallback.ts index 4319f3e0..965ecb61 100644 --- a/packages/callback-example/lib/IssuerCallback.ts +++ b/packages/callback-example/lib/IssuerCallback.ts @@ -3,7 +3,7 @@ import { Ed25519Signature2020 } from '@digitalcredentials/ed25519-signature-2020 import { Ed25519VerificationKey2020 } from '@digitalcredentials/ed25519-verification-key-2020' import { securityLoader } from '@digitalcredentials/security-document-loader' import vc from '@digitalcredentials/vc' -import { CredentialRequestV1_0_11 } from '@sphereon/oid4vci-common' +import { CredentialRequestV1_0_12 } from '@sphereon/oid4vci-common' import { ICredential, W3CVerifiableCredential } from '@sphereon/ssi-types' // Example on how to generate a did:key to issue a verifiable credential @@ -19,7 +19,7 @@ export const getIssuerCallback = (credential: ICredential, keyPair: any, verific throw new Error('A credential needs to be provided') } // eslint-disable-next-line @typescript-eslint/no-unused-vars - return async (_opts: { credentialRequest?: CredentialRequestV1_0_11; credential?: ICredential }): Promise => { + return async (_opts: { credentialRequest?: CredentialRequestV1_0_12; credential?: ICredential }): Promise => { const documentLoader = securityLoader().build() // eslint-disable-next-line @typescript-eslint/no-explicit-any const verificationKey: any = Array.from(keyPair.values())[0] diff --git a/packages/callback-example/lib/__tests__/issuerCallback.spec.ts b/packages/callback-example/lib/__tests__/issuerCallback.spec.ts index 16ae66d2..fa1c1fb2 100644 --- a/packages/callback-example/lib/__tests__/issuerCallback.spec.ts +++ b/packages/callback-example/lib/__tests__/issuerCallback.spec.ts @@ -13,7 +13,7 @@ import { ProofOfPossession, } from '@sphereon/oid4vci-common' import { CredentialOfferSession } from '@sphereon/oid4vci-common/dist' -import { CredentialSupportedBuilderV1_11, VcIssuer, VcIssuerBuilder } from '@sphereon/oid4vci-issuer' +import { CredentialSupportedBuilderV1_12, VcIssuer, VcIssuerBuilder } from '@sphereon/oid4vci-issuer' import { MemoryStates } from '@sphereon/oid4vci-issuer' import { CredentialDataSupplierResult } from '@sphereon/oid4vci-issuer/dist/types' import { ICredential, IProofPurpose, IProofType, W3CVerifiableCredential } from '@sphereon/ssi-types' @@ -85,7 +85,7 @@ describe('issuerCallback', () => { const clientId = 'sphereon:wallet' beforeAll(async () => { - const credentialsSupported: CredentialSupported = new CredentialSupportedBuilderV1_11() + const credentialsSupported: CredentialSupported = new CredentialSupportedBuilderV1_12() .withCryptographicSuitesSupported('ES256K') .withCryptographicBindingMethod('did') .withFormat('jwt_vc_json') @@ -236,7 +236,7 @@ describe('issuerCallback', () => { callbacks: { signCallback: proofOfPossessionCallbackFunction, }, - version: OpenId4VCIVersion.VER_1_0_11, + version: OpenId4VCIVersion.VER_1_0_12, }) .withClientId(clientId) .withKid(kid) @@ -247,7 +247,7 @@ describe('issuerCallback', () => { credentialTypes: ['VerifiableCredential'], format: 'jwt_vc_json', proofInput: proof, - version: OpenId4VCIVersion.VER_1_0_11, + version: OpenId4VCIVersion.VER_1_0_12, }) expect(credentialRequest).toEqual({ format: 'jwt_vc_json', diff --git a/packages/client/lib/AccessTokenClient.ts b/packages/client/lib/AccessTokenClient.ts index bab1788f..82601119 100644 --- a/packages/client/lib/AccessTokenClient.ts +++ b/packages/client/lib/AccessTokenClient.ts @@ -74,7 +74,7 @@ export class AccessTokenClient { metadata: metadata ? metadata : issuerOpts?.fetchMetadata - ? await MetadataClient.retrieveAllMetadata(issuerOpts.issuer, { errorOnNotFound: false }) + ? await MetadataClient.retrieveAllMetadata([issuerOpts.issuer], { errorOnNotFound: false }) // TODO multi-server support? : undefined, }); diff --git a/packages/client/lib/CredentialOfferClient.ts b/packages/client/lib/CredentialOfferClient.ts index 9d416853..f8f225c2 100644 --- a/packages/client/lib/CredentialOfferClient.ts +++ b/packages/client/lib/CredentialOfferClient.ts @@ -1,7 +1,5 @@ import { CredentialOffer, - CredentialOfferPayload, - CredentialOfferPayloadV1_0_09, CredentialOfferRequestWithBaseUrl, CredentialOfferV1_0_12, determineSpecVersionFromURI, @@ -24,24 +22,12 @@ export class CredentialOfferClient { const scheme = uri.split('://')[0]; const baseUrl = uri.split('?')[0]; const version = determineSpecVersionFromURI(uri); - let credentialOffer: CredentialOffer; - let credentialOfferPayload: CredentialOfferPayload; - 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'], - }) as CredentialOfferPayloadV1_0_09; - credentialOffer = { - credential_offer: credentialOfferPayload, - }; - } else { - credentialOffer = convertURIToJsonObject(uri, { - arrayTypeProperties: ['credentials'], - 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 credentialOffer: CredentialOffer = convertURIToJsonObject(uri, { + arrayTypeProperties: ['credentials'], + requiredProperties: uri.includes('credential_offer_uri=') ? ['credential_offer_uri'] : ['credential_offer'], + }) as CredentialOfferV1_0_12; + 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, { ...opts, @@ -77,7 +63,7 @@ export class CredentialOfferClient { const isUri = requestWithBaseUrl.credential_offer_uri !== undefined; - if (version.valueOf() >= OpenId4VCIVersion.VER_1_0_11.valueOf()) { + if (version.valueOf() >= OpenId4VCIVersion.VER_1_0_12.valueOf()) { // v11 changed from encoding every param to a encoded json object with a credential_offer param key if (!baseUrl.includes('?')) { param = isUri ? 'credential_offer_uri' : 'credential_offer'; @@ -98,7 +84,7 @@ export class CredentialOfferClient { arrayTypeProperties: isUri ? [] : ['credential_type'], uriTypeProperties: isUri ? ['credential_offer_uri'] - : version >= OpenId4VCIVersion.VER_1_0_11 + : version >= OpenId4VCIVersion.VER_1_0_12 ? ['credential_issuer', 'credential_type'] : ['issuer', 'credential_type'], param, diff --git a/packages/client/lib/CredentialRequestClient.ts b/packages/client/lib/CredentialRequestClient.ts index 8bcdfb89..4805a2ca 100644 --- a/packages/client/lib/CredentialRequestClient.ts +++ b/packages/client/lib/CredentialRequestClient.ts @@ -133,10 +133,10 @@ export class CredentialRequestClient { } private version(): OpenId4VCIVersion { - return this.credentialRequestOpts?.version ?? OpenId4VCIVersion.VER_1_0_11; + return this.credentialRequestOpts?.version ?? OpenId4VCIVersion.VER_1_0_12; } private isV11OrHigher(): boolean { - return this.version() >= OpenId4VCIVersion.VER_1_0_11; + return this.version() >= OpenId4VCIVersion.VER_1_0_12; } } diff --git a/packages/client/lib/CredentialRequestClientBuilder.ts b/packages/client/lib/CredentialRequestClientBuilder.ts index c3a0498e..b735ac27 100644 --- a/packages/client/lib/CredentialRequestClientBuilder.ts +++ b/packages/client/lib/CredentialRequestClientBuilder.ts @@ -1,7 +1,6 @@ import { AccessTokenResponse, CredentialIssuerMetadata, - CredentialOfferPayloadV1_0_08, CredentialOfferRequestWithBaseUrl, determineSpecVersionFromOffer, EndpointMetadata, @@ -42,13 +41,8 @@ export class CredentialRequestClientBuilder { builder.withVersion(version); builder.withCredentialEndpoint(metadata?.credential_endpoint ?? (issuer.endsWith('/') ? `${issuer}credential` : `${issuer}/credential`)); - if (version <= OpenId4VCIVersion.VER_1_0_08) { - //todo: This basically sets all types available during initiation. Probably the user only wants a subset. So do we want to do this? - builder.withCredentialType((request.original_credential_offer as CredentialOfferPayloadV1_0_08).credential_type); - } else { - // todo: look whether this is correct - builder.withCredentialType(getTypesFromOffer(request.credential_offer)); - } + // todo: look whether this is correct + builder.withCredentialType(getTypesFromOffer(request.credential_offer)); return builder; } @@ -104,7 +98,7 @@ export class CredentialRequestClientBuilder { public build(): CredentialRequestClient { if (!this.version) { - this.withVersion(OpenId4VCIVersion.VER_1_0_11); + this.withVersion(OpenId4VCIVersion.VER_1_0_12); } return new CredentialRequestClient(this); } diff --git a/packages/client/lib/MetadataClient.ts b/packages/client/lib/MetadataClient.ts index 38af66b0..c6d689fb 100644 --- a/packages/client/lib/MetadataClient.ts +++ b/packages/client/lib/MetadataClient.ts @@ -30,34 +30,44 @@ export class MetadataClient { * @param request */ public static async retrieveAllMetadataFromCredentialOfferRequest(request: CredentialOfferPayload): Promise { - const issuer = getIssuerFromCredentialOfferPayload(request); + const issuer = getIssuerFromCredentialOfferPayload(request); // TODO support multi-hosts? if (issuer) { - return MetadataClient.retrieveAllMetadata(issuer); + return MetadataClient.retrieveAllMetadata([issuer]); } throw new Error("can't retrieve metadata from CredentialOfferRequest. No issuer field is present"); } /** * Retrieve all metadata from an issuer - * @param issuer The issuer URL + * @param issuerHosts The issuer URL * @param opts */ - public static async retrieveAllMetadata(issuer: string, opts?: { errorOnNotFound: boolean }): Promise { + public static async retrieveAllMetadata(issuerHosts: string[], opts?: { errorOnNotFound: boolean }): Promise { let token_endpoint: string | undefined; let credential_endpoint: string | undefined; let authorization_endpoint: string | undefined; let authorizationServerType: AuthorizationServerType = 'OID4VCI'; - let authorization_server: string = issuer; - const oid4vciResponse = await MetadataClient.retrieveOpenID4VCIServerMetadata(issuer, { errorOnNotFound: false }); // We will handle errors later, given we will also try other metadata locations + let authorization_servers: string[] = issuerHosts; + const oid4vciResponse = await MetadataClient.retrieveOpenID4VCIServerMetadata(issuerHosts, { errorOnNotFound: false }); // We will handle errors later, given we will also try other metadata locations + let issuerHost = oid4vciResponse?.selectedHost + if (issuerHost) { + // Move the selected issuerHost to the beginning of the array so teh consecutive calls prefer the same server TODO is this even useful? + const index = authorization_servers.indexOf(issuerHost); + if (index > -1) { + authorization_servers.splice(index, 1); + } + authorization_servers.unshift(issuerHost); + } + let credentialIssuerMetadata = oid4vciResponse?.successBody; if (credentialIssuerMetadata) { - debug(`Issuer ${issuer} OID4VCI well-known server metadata\r\n${JSON.stringify(credentialIssuerMetadata)}`); + debug(`Issuer ${issuerHost} OID4VCI well-known server metadata\r\n${JSON.stringify(credentialIssuerMetadata)}`); credential_endpoint = credentialIssuerMetadata.credential_endpoint; if (credentialIssuerMetadata.token_endpoint) { token_endpoint = credentialIssuerMetadata.token_endpoint; } if (credentialIssuerMetadata.authorization_servers) { - authorization_server = credentialIssuerMetadata.authorization_servers; + authorization_servers = credentialIssuerMetadata.authorization_servers; } if (credentialIssuerMetadata.authorization_endpoint) { authorization_endpoint = credentialIssuerMetadata.authorization_endpoint; @@ -65,34 +75,43 @@ export class MetadataClient { } // No specific OID4VCI endpoint. Either can be an OAuth2 AS or an OIDC IDP. Let's start with OIDC first let response: OpenIDResponse = await MetadataClient.retrieveWellknown( - authorization_server, + authorization_servers, WellKnownEndpoints.OPENID_CONFIGURATION, { errorOnNotFound: false, }, ); + if(response?.selectedHost) { + issuerHost = response?.selectedHost + } let authMetadata = response.successBody; if (authMetadata) { - debug(`Issuer ${issuer} has OpenID Connect Server metadata in well-known location`); + debug(`Issuer ${issuerHost} has OpenID Connect Server metadata in well-known location`); authorizationServerType = 'OIDC'; } else { // Now let's do OAuth2 - response = await MetadataClient.retrieveWellknown(authorization_server, WellKnownEndpoints.OAUTH_AS, { errorOnNotFound: false }); + response = await MetadataClient.retrieveWellknown(authorization_servers, WellKnownEndpoints.OAUTH_AS, { errorOnNotFound: false }); authMetadata = response.successBody; + if(response?.selectedHost) { + issuerHost = response?.selectedHost + } } if (!authMetadata) { // We will always throw an error, no matter whether the user provided the option not to, because this is bad. - if (issuer !== authorization_server) { - throw Error(`Issuer ${issuer} provided a separate authorization server ${authorization_server}, but that server did not provide metadata`); + if(!issuerHost) { + throw Error(`None of provided authorization servers ${authorization_servers} returned a response.`); + } + if (!authorization_servers.includes(issuerHost)) { + throw Error(`Issuer ${issuerHost} provided separate authorization servers ${authorization_servers}, but those servers did not provide metadata`); } } else { if (!authorizationServerType) { authorizationServerType = 'OAuth 2.0'; } - debug(`Issuer ${issuer} has ${authorizationServerType} Server metadata in well-known location`); + debug(`Issuer ${issuerHost} has ${authorizationServerType} Server metadata in well-known location`); if (!authMetadata.authorization_endpoint) { console.warn( - `Issuer ${issuer} of type ${authorizationServerType} has no authorization_endpoint! Will use ${authorization_endpoint}. This only works for pre-authorized flows`, + `Issuer ${issuerHost} of type ${authorizationServerType} has no authorization_endpoint! Will use ${authorization_endpoint}. This only works for pre-authorized flows`, ); } else if (authorization_endpoint && authMetadata.authorization_endpoint !== authorization_endpoint) { throw Error( @@ -101,7 +120,7 @@ export class MetadataClient { } authorization_endpoint = authMetadata.authorization_endpoint; if (!authMetadata.token_endpoint) { - throw Error(`Authorization Sever ${authorization_server} did not provide a token_endpoint`); + throw Error(`Authorization Sever ${authorization_servers} did not provide a token_endpoint`); } else if (token_endpoint && authMetadata.token_endpoint !== token_endpoint) { throw Error( `Credential issuer has a different token_endpoint (${token_endpoint}) from the Authorization Server (${authMetadata.token_endpoint})`, @@ -120,22 +139,22 @@ export class MetadataClient { } if (!authorization_endpoint) { - debug(`Issuer ${issuer} does not expose authorization_endpoint, so only pre-auth will be supported`); + debug(`Issuer ${issuerHost} does not expose authorization_endpoint, so only pre-auth will be supported`); } if (!token_endpoint) { - debug(`Issuer ${issuer} does not have a token_endpoint listed in well-known locations!`); - if (opts?.errorOnNotFound) { - throw Error(`Could not deduce the token_endpoint for ${issuer}`); + debug(`Issuer ${issuerHost} does not have a token_endpoint listed in well-known locations!`); + if (opts?.errorOnNotFound || !issuerHost) { + throw Error(`Could not deduce the token_endpoint for ${issuerHost}`); } else { - token_endpoint = `${issuer}${issuer.endsWith('/') ? 'token' : '/token'}`; + token_endpoint = `${issuerHost}${issuerHost.endsWith('/') ? 'token' : '/token'}`; } } if (!credential_endpoint) { - debug(`Issuer ${issuer} does not have a credential_endpoint listed in well-known locations!`); - if (opts?.errorOnNotFound) { - throw Error(`Could not deduce the credential endpoint for ${issuer}`); + debug(`Issuer ${issuerHost} does not have a credential_endpoint listed in well-known locations!`); + if (opts?.errorOnNotFound || !issuerHost) { + throw Error(`Could not deduce the credential endpoint for ${issuerHost}`); } else { - credential_endpoint = `${issuer}${issuer.endsWith('/') ? 'credential' : '/credential'}`; + credential_endpoint = `${issuerHost}${issuerHost.endsWith('/') ? 'credential' : '/credential'}`; } } @@ -143,12 +162,15 @@ export class MetadataClient { // Apparently everything worked out and the issuer is exposing everything in oAuth2/OIDC well-knowns. Spec is vague about this situation, but we can support it credentialIssuerMetadata = authMetadata as CredentialIssuerMetadata; } - debug(`Issuer ${issuer} token endpoint ${token_endpoint}, credential endpoint ${credential_endpoint}`); + if(!issuerHost) { + throw Error(`None of provided authorization servers ${authorization_servers} returned a response.`); + } + debug(`Issuer ${issuerHost} token endpoint ${token_endpoint}, credential endpoint ${credential_endpoint}`); return { - issuer, + issuer: issuerHost, token_endpoint, credential_endpoint, - authorization_server, + authorization_servers: authorization_servers, authorization_endpoint, authorizationServerType, credentialIssuerMetadata: credentialIssuerMetadata, @@ -159,15 +181,15 @@ export class MetadataClient { /** * Retrieve only the OID4VCI metadata for the issuer. So no OIDC/OAuth2 metadata * - * @param issuerHost The issuer hostname + * @param issuerHosts The issuer hostname */ public static async retrieveOpenID4VCIServerMetadata( - issuerHost: string, + issuerHosts: string[], opts?: { errorOnNotFound?: boolean; }, ): Promise | undefined> { - return MetadataClient.retrieveWellknown(issuerHost, WellKnownEndpoints.OPENID4VCI_ISSUER, { + return MetadataClient.retrieveWellknown(issuerHosts, WellKnownEndpoints.OPENID4VCI_ISSUER, { errorOnNotFound: opts?.errorOnNotFound === undefined ? true : opts.errorOnNotFound, }); } @@ -175,22 +197,51 @@ export class MetadataClient { /** * Allows to retrieve information from a well-known location * - * @param host The host + * @param hosts The list hosts * @param endpointType The endpoint type, currently supports OID4VCI, OIDC and OAuth2 endpoint types * @param opts Options, like for instance whether an error should be thrown in case the endpoint doesn't exist */ + public static async retrieveWellknown( - host: string, + hosts: string[], endpointType: WellKnownEndpoints, - opts?: { errorOnNotFound?: boolean }, + opts?: { errorOnNotFound?: boolean } ): Promise> { - const result: OpenIDResponse = await getJson(`${host.endsWith('/') ? host.slice(0, -1) : host}${endpointType}`, { - exceptionOnHttpErrorStatus: opts?.errorOnNotFound, - }); - if (result.origResponse.status >= 400) { - // We only get here when error on not found is false - debug(`host ${host} with endpoint type ${endpointType} status: ${result.origResponse.status}, ${result.origResponse.statusText}`); + let lastError: string | undefined; + + for (const host of hosts) { + try { + const result: OpenIDResponse = await getJson(`${host.endsWith('/') ? host.slice(0, -1) : host}${endpointType}`, { + exceptionOnHttpErrorStatus: opts?.errorOnNotFound + }); + + if (result.origResponse.status >= 400) { + debug(`host ${host} with endpoint type ${endpointType} status: ${result.origResponse.status}, ${result.origResponse.statusText}`); + if (hosts.indexOf(host) < hosts.length - 1) { + continue; // Try the next host + } + } + result.selectedHost = host + return result; + } catch (error) { + let message: string; + if (error instanceof Error) { + message = `host ${host} error: ${error.message}`; + } else { + message = `host ${host} encountered an unknown error`; + } + if (!lastError) { + lastError = message; + } else { + lastError += `\r\n${message}`; + } + } + } + + if (lastError) { + throw lastError; + } else { + throw new Error('All hosts failed to retrieve well-known endpoint'); } - return result; } } diff --git a/packages/client/lib/OpenID4VCIClient.ts b/packages/client/lib/OpenID4VCIClient.ts index cdba517b..9a249376 100644 --- a/packages/client/lib/OpenID4VCIClient.ts +++ b/packages/client/lib/OpenID4VCIClient.ts @@ -3,7 +3,6 @@ import { Alg, AuthzFlowType, CodeChallengeMethod, - CredentialOfferPayloadV1_0_08, CredentialOfferRequestWithBaseUrl, CredentialResponse, CredentialSupported, @@ -13,10 +12,9 @@ import { OpenId4VCIVersion, ProofOfPossessionCallbacks, PushedAuthorizationResponse, - ResponseType, + ResponseType } from '@sphereon/oid4vci-common'; 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'; @@ -325,9 +323,15 @@ export class OpenID4VCIClient { throw Error(`Not all credential types ${JSON.stringify(credentialTypes)} are supported by issuer ${this.getIssuer()}`); } } else if (metadata.credentials_supported && !Array.isArray(metadata.credentials_supported)) { - const credentialsSupported = metadata.credentials_supported as CredentialSupportedTypeV1_0_08; - if (types.some((type) => !metadata.credentials_supported || !credentialsSupported[type])) { - throw Error(`Not all credential types ${JSON.stringify(credentialTypes)} are supported by issuer ${this.getIssuer()}`); + const credentialsSupported = metadata.credentials_supported as CredentialSupported; + if (credentialsSupported.format === 'vc+sd-jwt') { + if (types.some((type) => !metadata.credentials_supported || credentialsSupported.credential_definition.vct === type)) { // TODO why are we doing iterating types and not using them? + throw Error(`Not all credential types ${JSON.stringify(credentialTypes)} are supported by issuer ${this.getIssuer()}`); + } + } else { + if (types.some((type) => !metadata.credentials_supported || !credentialsSupported.credential_definition.type.includes(type))) { + throw Error(`Not all credential types ${JSON.stringify(credentialTypes)} are supported by issuer ${this.getIssuer()}`); + } } } // todo: Format check? We might end up with some disjoint type / format combinations supported by the server @@ -387,25 +391,17 @@ export class OpenID4VCIClient { } getCredentialOfferTypes(): string[][] { - if (this.credentialOffer.version < OpenId4VCIVersion.VER_1_0_11) { - const orig = this.credentialOffer.original_credential_offer as CredentialOfferPayloadV1_0_08; - const types: string[] = typeof orig.credential_type === 'string' ? [orig.credential_type] : orig.credential_type; - const result: string[][] = []; - result[0] = types; - return result; - } else { - return this.credentialOffer.credential_offer.credentials.map((c) => { - if (typeof c === 'string') { - return [c]; - } else if ('types' in c) { - return c.types; - } else if ('vct' in c.credential_definition) { - return [c.credential_definition.vct]; - } else { - return c.credential_definition.types; - } - }); - } + return this.credentialOffer.credential_offer.credentials.map((c) => { + if (typeof c === 'string') { + return [c]; + } else if ('types' in c) { + return c.types; + } else if ('vct' in c.credential_definition) { + return [c.credential_definition.vct]; + } else { + return c.credential_definition.types; + } + }); } issuerSupportedFlowTypes(): AuthzFlowType[] { diff --git a/packages/client/lib/ProofOfPossessionBuilder.ts b/packages/client/lib/ProofOfPossessionBuilder.ts index ac402cf0..8d7bb129 100644 --- a/packages/client/lib/ProofOfPossessionBuilder.ts +++ b/packages/client/lib/ProofOfPossessionBuilder.ts @@ -46,7 +46,7 @@ export class ProofOfPossessionBuilder { if (jwt) { this.withJwt(jwt); } else { - this.withTyp(version < OpenId4VCIVersion.VER_1_0_11 ? 'jwt' : 'openid4vci-proof+jwt'); + this.withTyp(version < OpenId4VCIVersion.VER_1_0_12 ? 'jwt' : 'openid4vci-proof+jwt'); } if (accessTokenResponse) { this.withAccessTokenResponse(accessTokenResponse); @@ -107,7 +107,7 @@ export class ProofOfPossessionBuilder { } withTyp(typ: Typ): this { - if (this.version >= OpenId4VCIVersion.VER_1_0_11) { + if (this.version >= OpenId4VCIVersion.VER_1_0_12) { if (!!typ && typ !== 'openid4vci-proof+jwt') { throw Error('typ must be openid4vci-proof+jwt for version 1.0.11 and up'); } @@ -154,7 +154,7 @@ export class ProofOfPossessionBuilder { if (jwt.header.typ) { this.withTyp(jwt.header.typ as Typ); } - if (this.version >= OpenId4VCIVersion.VER_1_0_11) { + if (this.version >= OpenId4VCIVersion.VER_1_0_12) { this.withTyp('openid4vci-proof+jwt'); } this.withAlg(jwt.header.alg); @@ -180,7 +180,7 @@ export class ProofOfPossessionBuilder { return await createProofOfPossession( this.callbacks, { - typ: this.typ ?? (this.version < OpenId4VCIVersion.VER_1_0_11 ? 'jwt' : 'openid4vci-proof+jwt'), + typ: this.typ ?? (this.version < OpenId4VCIVersion.VER_1_0_12 ? 'jwt' : 'openid4vci-proof+jwt'), kid: this.kid, jti: this.jti, alg: this.alg, diff --git a/packages/client/lib/__tests__/IT.spec.ts b/packages/client/lib/__tests__/IT.spec.ts index 00ca84ea..6120afee 100644 --- a/packages/client/lib/__tests__/IT.spec.ts +++ b/packages/client/lib/__tests__/IT.spec.ts @@ -154,7 +154,7 @@ describe('OID4VCI-Client should', () => { callbacks: { signCallback: proofOfPossessionCallbackFunction, }, - version: OpenId4VCIVersion.VER_1_0_11, + version: OpenId4VCIVersion.VER_1_0_12, }) .withEndpointMetadata({ issuer: 'https://issuer.research.identiproof.io', diff --git a/packages/client/lib/__tests__/IssuanceInitiation.spec.ts b/packages/client/lib/__tests__/IssuanceInitiation.spec.ts index f28e1f02..b54c970f 100644 --- a/packages/client/lib/__tests__/IssuanceInitiation.spec.ts +++ b/packages/client/lib/__tests__/IssuanceInitiation.spec.ts @@ -52,7 +52,7 @@ describe('Issuance Initiation', () => { const client = await CredentialOfferClient.fromURI( 'openid-credential-offer://?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.version).toEqual(OpenId4VCIVersion.VER_1_0_12); expect(client.baseUrl).toEqual('openid-credential-offer://'); expect(client.scheme).toEqual('openid-credential-offer'); expect(client.credential_offer.credential_issuer).toEqual('https://launchpad.vii.electron.mattrlabs.io'); diff --git a/packages/client/lib/__tests__/ProofOfPossessionBuilder.spec.ts b/packages/client/lib/__tests__/ProofOfPossessionBuilder.spec.ts index a4ef65d3..2890e1ca 100644 --- a/packages/client/lib/__tests__/ProofOfPossessionBuilder.spec.ts +++ b/packages/client/lib/__tests__/ProofOfPossessionBuilder.spec.ts @@ -44,7 +44,7 @@ beforeAll(async () => { describe('ProofOfPossession Builder ', () => { it('should fail without supplied proof or callbacks', async function () { await expect( - ProofOfPossessionBuilder.fromProof(undefined as never, OpenId4VCIVersion.VER_1_0_11) + ProofOfPossessionBuilder.fromProof(undefined as never, OpenId4VCIVersion.VER_1_0_12) .withIssuer(IDENTIPROOF_ISSUER_URL) .withClientId('sphereon:wallet') .withKid(kid) diff --git a/packages/common/lib/__tests__/CredentialOfferUtil.spec.ts b/packages/common/lib/__tests__/CredentialOfferUtil.spec.ts index a8278299..a8c1b134 100644 --- a/packages/common/lib/__tests__/CredentialOfferUtil.spec.ts +++ b/packages/common/lib/__tests__/CredentialOfferUtil.spec.ts @@ -30,7 +30,7 @@ describe('CredentialOfferUtil should', () => { it( 'get version 11 with sample URL', async () => { - expect(determineSpecVersionFromURI(CREDENTIAL_OFFER_QR_V11)).toEqual(OpenId4VCIVersion.VER_1_0_11); + expect(determineSpecVersionFromURI(CREDENTIAL_OFFER_QR_V11)).toEqual(OpenId4VCIVersion.VER_1_0_12); }, UNIT_TEST_TIMEOUT, ); @@ -48,7 +48,7 @@ describe('CredentialOfferUtil should', () => { it( 'get version 11 as default value', async () => { - expect(determineSpecVersionFromURI('test://uri')).toEqual(OpenId4VCIVersion.VER_1_0_11); + expect(determineSpecVersionFromURI('test://uri')).toEqual(OpenId4VCIVersion.VER_1_0_12); }, UNIT_TEST_TIMEOUT, ); diff --git a/packages/common/lib/functions/CredentialOfferUtil.ts b/packages/common/lib/functions/CredentialOfferUtil.ts index 7f2428f1..43b94a0b 100644 --- a/packages/common/lib/functions/CredentialOfferUtil.ts +++ b/packages/common/lib/functions/CredentialOfferUtil.ts @@ -5,9 +5,7 @@ import { AuthzFlowType, CredentialOffer, CredentialOfferPayload, - CredentialOfferPayloadV1_0_08, - CredentialOfferPayloadV1_0_09, - CredentialOfferPayloadV1_0_11, + CredentialOfferPayloadV1_0_12, DefaultURISchemes, Grant, GrantTypes, @@ -26,29 +24,31 @@ export function determineSpecVersionFromURI(uri: string): OpenId4VCIVersion { let version: OpenId4VCIVersion = OpenId4VCIVersion.VER_UNKNOWN; version = determineSpecVersionFromScheme(uri, version); - version = getVersionFromURIParam(uri, version, OpenId4VCIVersion.VER_1_0_08, 'initiate_issuance'); - version = getVersionFromURIParam(uri, version, OpenId4VCIVersion.VER_1_0_08, 'credential_type'); - version = getVersionFromURIParam(uri, version, OpenId4VCIVersion.VER_1_0_08, 'op_state'); + version = getVersionFromURIParam(uri, version, OpenId4VCIVersion.VER_UNSUPPORTED, 'initiate_issuance'); + version = getVersionFromURIParam(uri, version, OpenId4VCIVersion.VER_UNSUPPORTED, 'credential_type'); + version = getVersionFromURIParam(uri, version, OpenId4VCIVersion.VER_UNSUPPORTED, 'op_state'); // version = getVersionFromURIParam(uri, version, OpenId4VCIVersion.VER_1_0_09, 'credentials'); // version = getVersionFromURIParam(uri, version, OpenId4VCIVersion.VER_1_0_09, 'initiate_issuance_uri') - version = getVersionFromURIParam(uri, version, OpenId4VCIVersion.VER_1_0_11, 'credential_offer_uri='); - version = getVersionFromURIParam(uri, version, OpenId4VCIVersion.VER_1_0_11, 'credential_issuer'); - version = getVersionFromURIParam(uri, version, OpenId4VCIVersion.VER_1_0_11, 'grants'); + version = getVersionFromURIParam(uri, version, OpenId4VCIVersion.VER_1_0_12, 'credential_offer_uri='); + version = getVersionFromURIParam(uri, version, OpenId4VCIVersion.VER_1_0_12, 'credential_issuer'); + version = getVersionFromURIParam(uri, version, OpenId4VCIVersion.VER_1_0_12, 'grants'); - if (version === OpenId4VCIVersion.VER_UNKNOWN) { - version = OpenId4VCIVersion.VER_1_0_11; +/* + if (version === OpenId4VCIVersion.VER_UNKNOWN) { Why would we impose a version when required parameters are missing? If we need it for a specific case, please add a comment. + version = OpenId4VCIVersion.VER_1_0_12; } +*/ return version; } export function determineSpecVersionFromScheme(credentialOfferURI: string, openId4VCIVersion: OpenId4VCIVersion) { const scheme = getScheme(credentialOfferURI); if (credentialOfferURI.includes(DefaultURISchemes.INITIATE_ISSUANCE)) { - return recordVersion(openId4VCIVersion, OpenId4VCIVersion.VER_1_0_08, scheme); + return recordVersion(openId4VCIVersion, OpenId4VCIVersion.VER_UNSUPPORTED, scheme); } else if (credentialOfferURI.includes(DefaultURISchemes.CREDENTIAL_OFFER)) { - return recordVersion(openId4VCIVersion, OpenId4VCIVersion.VER_1_0_11, scheme); + return recordVersion(openId4VCIVersion, OpenId4VCIVersion.VER_1_0_12, scheme); } else { return recordVersion(openId4VCIVersion, OpenId4VCIVersion.VER_UNKNOWN, scheme); } @@ -61,20 +61,16 @@ export function getScheme(credentialOfferURI: string) { return credentialOfferURI.split('://')[0]; } -export function getIssuerFromCredentialOfferPayload(request: CredentialOfferPayload): string | undefined { +export function getIssuerFromCredentialOfferPayload(request: CredentialOfferPayload): string | undefined{ if (!request || (!('issuer' in request) && !('credential_issuer' in request))) { return undefined; } - return 'issuer' in request ? request.issuer : request['credential_issuer']; + return request['credential_issuer']; } export function determineSpecVersionFromOffer(offer: CredentialOfferPayload | CredentialOffer): OpenId4VCIVersion { - if (isCredentialOfferV1_0_11(offer)) { - return OpenId4VCIVersion.VER_1_0_11; - } else if (isCredentialOfferV1_0_09(offer)) { - return OpenId4VCIVersion.VER_1_0_09; - } else if (isCredentialOfferV1_0_08(offer)) { - return OpenId4VCIVersion.VER_1_0_08; + if (isCredentialOfferV1_0_12(offer)) { + return OpenId4VCIVersion.VER_1_0_12; } return OpenId4VCIVersion.VER_UNKNOWN; } @@ -94,37 +90,7 @@ export function isCredentialOfferVersion(offer: CredentialOfferPayload | Credent return true; } -function isCredentialOfferV1_0_08(offer: CredentialOfferPayload | CredentialOffer): boolean { - if (!offer) { - return false; - } - if ('issuer' in offer && 'credential_type' in offer) { - // payload - return true; - } - if ('credential_offer' in offer && offer['credential_offer']) { - // offer, so check payload - return isCredentialOfferV1_0_08(offer['credential_offer']); - } - return false; -} - -function isCredentialOfferV1_0_09(offer: CredentialOfferPayload | CredentialOffer): boolean { - if (!offer) { - return false; - } - if ('issuer' in offer && 'credentials' in offer) { - // payload - return true; - } - if ('credential_offer' in offer && offer['credential_offer']) { - // offer, so check payload - return isCredentialOfferV1_0_09(offer['credential_offer']); - } - return false; -} - -function isCredentialOfferV1_0_11(offer: CredentialOfferPayload | CredentialOffer): boolean { +function isCredentialOfferV1_0_12(offer: CredentialOfferPayload | CredentialOffer): boolean { if (!offer) { return false; } @@ -134,7 +100,7 @@ function isCredentialOfferV1_0_11(offer: CredentialOfferPayload | CredentialOffe } if ('credential_offer' in offer && offer['credential_offer']) { // offer, so check payload - return isCredentialOfferV1_0_11(offer['credential_offer']); + return isCredentialOfferV1_0_12(offer['credential_offer']); } return 'credential_offer_uri' in offer; } @@ -152,7 +118,7 @@ export async function toUniformCredentialOfferRequest( if ('credential_offer_uri' in offer && offer?.credential_offer_uri !== undefined) { credentialOfferURI = offer.credential_offer_uri; if (opts?.resolve || opts?.resolve === undefined) { - originalCredentialOffer = (await resolveCredentialOfferURI(credentialOfferURI)) as CredentialOfferPayloadV1_0_11; + originalCredentialOffer = (await resolveCredentialOfferURI(credentialOfferURI)) as CredentialOfferPayloadV1_0_12; } else if (!originalCredentialOffer) { throw Error(`Credential offer uri (${credentialOfferURI}) found, but resolution was explicitly disabled and credential_offer was supplied`); } @@ -216,52 +182,12 @@ export function toUniformCredentialOfferPayload( ): UniformCredentialOfferPayload { // todo: create test to check idempotence once a payload is already been made uniform. const version = opts?.version ?? determineSpecVersionFromOffer(offer); - if (version >= OpenId4VCIVersion.VER_1_0_11) { + if (version >= OpenId4VCIVersion.VER_1_0_12) { const orig = offer as UniformCredentialOfferPayload; return { ...orig, }; } - const grants: Grant = 'grants' in offer ? (offer.grants as Grant) : {}; - let offerPayloadAsV8V9 = offer as CredentialOfferPayloadV1_0_08 | CredentialOfferPayloadV1_0_09; - if (isCredentialOfferVersion(offer, OpenId4VCIVersion.VER_1_0_08, OpenId4VCIVersion.VER_1_0_09)) { - if (offerPayloadAsV8V9.op_state) { - grants.authorization_code = { - ...grants.authorization_code, - issuer_state: offerPayloadAsV8V9.op_state, - }; - } - let user_pin_required = false; - if (typeof offerPayloadAsV8V9.user_pin_required === 'string') { - user_pin_required = offerPayloadAsV8V9.user_pin_required === 'true' || offerPayloadAsV8V9.user_pin_required === 'yes'; - } else if (offerPayloadAsV8V9.user_pin_required !== undefined) { - user_pin_required = offerPayloadAsV8V9.user_pin_required; - } - if (offerPayloadAsV8V9['pre-authorized_code']) { - grants['urn:ietf:params:oauth:grant-type:pre-authorized_code'] = { - 'pre-authorized_code': offerPayloadAsV8V9['pre-authorized_code'], - user_pin_required, - }; - } - } - const issuer = getIssuerFromCredentialOfferPayload(offer); - if (version === OpenId4VCIVersion.VER_1_0_09) { - offerPayloadAsV8V9 = offer as CredentialOfferPayloadV1_0_09; - return { - // credential_definition: getCredentialsSupported(never, offerPayloadAsV8V9.credentials).map(sup => {credentialSubject: sup.credentialSubject})[0], - credential_issuer: issuer ?? offerPayloadAsV8V9.issuer, - credentials: offerPayloadAsV8V9.credentials, - grants, - }; - } - if (version === OpenId4VCIVersion.VER_1_0_08) { - offerPayloadAsV8V9 = offer as CredentialOfferPayloadV1_0_08; - return { - credential_issuer: issuer ?? offerPayloadAsV8V9.issuer, - credentials: Array.isArray(offerPayloadAsV8V9.credential_type) ? offerPayloadAsV8V9.credential_type : [offerPayloadAsV8V9.credential_type], - grants, - } as UniformCredentialOfferPayload; - } throw Error(`Could not create uniform payload for version ${version}`); } @@ -277,10 +203,6 @@ export function determineFlowType( if (payload.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.['pre-authorized_code']) { supportedFlows.push(AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW); } - if (supportedFlows.length === 0 && version < OpenId4VCIVersion.VER_1_0_09) { - // auth flow without op_state was possible in v08. The only way to know is that the detections would result in finding nothing. - supportedFlows.push(AuthzFlowType.AUTHORIZATION_CODE_FLOW); - } return supportedFlows; } diff --git a/packages/common/lib/functions/CredentialRequestUtil.ts b/packages/common/lib/functions/CredentialRequestUtil.ts index 66ee9fe6..43a150bc 100644 --- a/packages/common/lib/functions/CredentialRequestUtil.ts +++ b/packages/common/lib/functions/CredentialRequestUtil.ts @@ -1,6 +1,4 @@ -import { CredentialRequestV1_0_08, OpenId4VCIVersion, UniformCredentialRequest } from '../types'; - -import { getFormatForVersion } from './FormatUtils'; +import { OpenId4VCIVersion, UniformCredentialRequest } from '../types'; export function getTypesFromRequest(credentialRequest: UniformCredentialRequest, opts?: { filterVerifiableCredential: boolean }) { let types: string[] = []; @@ -24,17 +22,6 @@ export function getTypesFromRequest(credentialRequest: UniformCredentialRequest, export function getCredentialRequestForVersion( credentialRequest: UniformCredentialRequest, version: OpenId4VCIVersion, -): UniformCredentialRequest | CredentialRequestV1_0_08 { - if (version === OpenId4VCIVersion.VER_1_0_08) { - const draft8Format = getFormatForVersion(credentialRequest.format, version); - const types = getTypesFromRequest(credentialRequest, { filterVerifiableCredential: true }); - - return { - format: draft8Format, - proof: credentialRequest.proof, - type: types[0], - } satisfies CredentialRequestV1_0_08; - } - +): UniformCredentialRequest { return credentialRequest; } diff --git a/packages/common/lib/functions/Encoding.ts b/packages/common/lib/functions/Encoding.ts index e9310e29..6a5c9dc7 100644 --- a/packages/common/lib/functions/Encoding.ts +++ b/packages/common/lib/functions/Encoding.ts @@ -1,4 +1,4 @@ -import { BAD_PARAMS, DecodeURIAsJsonOpts, EncodeJsonAsURIOpts, JsonURIMode, OpenId4VCIVersion, SearchValue } from '../types'; +import { BAD_PARAMS, DecodeURIAsJsonOpts, EncodeJsonAsURIOpts, JsonURIMode, SearchValue } from '../types'; /** * @function encodeJsonAsURI encodes a Json object into a URI @@ -30,7 +30,7 @@ export function convertJsonToURI( } let components: string; - if ((opts?.version && opts.version > OpenId4VCIVersion.VER_1_0_08 && !opts.mode) || opts?.mode === JsonURIMode.JSON_STRINGIFY) { + if (opts?.mode === JsonURIMode.JSON_STRINGIFY) { // v11 changed from encoding every param to a encoded json object with a credential_offer param key components = encodeAndStripWhitespace(JSON.stringify(json)); } else { diff --git a/packages/common/lib/functions/FormatUtils.ts b/packages/common/lib/functions/FormatUtils.ts index 2eda8449..c49d7873 100644 --- a/packages/common/lib/functions/FormatUtils.ts +++ b/packages/common/lib/functions/FormatUtils.ts @@ -38,15 +38,5 @@ export function getUniformFormat(format: string | OID4VCICredentialFormat | Cred } export function getFormatForVersion(format: string, version: OpenId4VCIVersion) { - const uniformFormat = isUniformFormat(format) ? format : getUniformFormat(format); - - if (version === OpenId4VCIVersion.VER_1_0_08) { - if (uniformFormat === 'jwt_vc_json') { - return 'jwt_vc' as const; - } else if (uniformFormat === 'ldp_vc' || uniformFormat === 'jwt_vc_json-ld') { - return 'ldp_vc' as const; - } - } - - return uniformFormat; + return isUniformFormat(format) ? format : getUniformFormat(format); } diff --git a/packages/common/lib/functions/IssuerMetadataUtils.ts b/packages/common/lib/functions/IssuerMetadataUtils.ts index 40eb85d6..89cf6a51 100644 --- a/packages/common/lib/functions/IssuerMetadataUtils.ts +++ b/packages/common/lib/functions/IssuerMetadataUtils.ts @@ -2,16 +2,13 @@ import { CredentialIssuerMetadata, CredentialOfferFormat, CredentialSupported, - CredentialSupportedTypeV1_0_08, - CredentialSupportedV1_0_08, - IssuerMetadataV1_0_08, MetadataDisplay, OID4VCICredentialFormat, OpenId4VCIVersion, } from '../types'; export function getSupportedCredentials(opts?: { - issuerMetadata?: CredentialIssuerMetadata | IssuerMetadataV1_0_08; + issuerMetadata?: CredentialIssuerMetadata; version: OpenId4VCIVersion; types?: string[][]; format?: (OID4VCICredentialFormat | string) | (OID4VCICredentialFormat | string)[]; @@ -23,7 +20,7 @@ export function getSupportedCredentials(opts?: { } export function getSupportedCredential(opts?: { - issuerMetadata?: CredentialIssuerMetadata | IssuerMetadataV1_0_08; + issuerMetadata?: CredentialIssuerMetadata; version: OpenId4VCIVersion; types?: string | string[]; format?: (OID4VCICredentialFormat | string) | (OID4VCICredentialFormat | string)[]; @@ -37,16 +34,12 @@ export function getSupportedCredential(opts?: { } else { formats = []; } - let credentialsSupported: CredentialSupported[]; + if (!issuerMetadata) { return []; } - const { version, types } = opts ?? { version: OpenId4VCIVersion.VER_1_0_11 }; - if (version === OpenId4VCIVersion.VER_1_0_08 || !Array.isArray(issuerMetadata.credentials_supported)) { - credentialsSupported = credentialsSupportedV8ToV11((issuerMetadata as IssuerMetadataV1_0_08).credentials_supported); - } else { - credentialsSupported = (issuerMetadata as CredentialIssuerMetadata).credentials_supported; - } + const { types } = opts ?? {}; + const credentialsSupported: CredentialSupported[] = (issuerMetadata as CredentialIssuerMetadata).credentials_supported; if (credentialsSupported === undefined || credentialsSupported.length === 0) { return []; @@ -64,9 +57,6 @@ export function getSupportedCredential(opts?: { initiationTypes = opts.types; } } - if (version === OpenId4VCIVersion.VER_1_0_08 && (!initiationTypes || initiationTypes?.length === 0)) { - initiationTypes = formats; - } const supportedFormats: (CredentialOfferFormat | string)[] = formats && formats.length > 0 ? formats : ['jwt_vc_json', 'jwt_vc_json-ld', 'ldp_vc']; const credentialSupportedOverlap: CredentialSupported[] = []; @@ -101,12 +91,11 @@ export function getSupportedCredential(opts?: { export function getTypesFromCredentialSupported(credentialSupported: CredentialSupported, opts?: { filterVerifiableCredential: boolean }) { let types: string[] = []; - if (credentialSupported.format === 'jwt_vc_json' || credentialSupported.format === 'jwt_vc_json-ld' || credentialSupported.format === 'ldp_vc') { - types = credentialSupported.types; - } else if (credentialSupported.format === 'vc+sd-jwt') { + if (credentialSupported.format === 'vc+sd-jwt') { types = [credentialSupported.credential_definition.vct]; + } else { + types = credentialSupported.credential_definition.type; } - if (!types || types.length === 0) { throw Error('Could not deduce types from credential supported'); } @@ -127,33 +116,8 @@ function arrayEqualsIgnoreOrder(a: string[], b: string[]) { return true; } -export function credentialsSupportedV8ToV11(supportedV8: CredentialSupportedTypeV1_0_08): CredentialSupported[] { - return Object.entries(supportedV8).flatMap((entry) => { - const type = entry[0]; - const supportedV8 = entry[1]; - return credentialSupportedV8ToV11(type, supportedV8); - }); -} - -export function credentialSupportedV8ToV11(key: string, supportedV8: CredentialSupportedV1_0_08): CredentialSupported[] { - return Object.entries(supportedV8.formats).map((entry) => { - const format = entry[0]; - const credentialSupportBrief = entry[1]; - if (typeof format !== 'string') { - throw Error(`Unknown format received ${JSON.stringify(format)}`); - } - let credentialSupport: Partial = {}; - credentialSupport = { - format: format as OID4VCICredentialFormat, - display: supportedV8.display, - ...credentialSupportBrief, - credentialSubject: supportedV8.claims, - }; - return credentialSupport as CredentialSupported; - }); -} -export function getIssuerDisplays(metadata: CredentialIssuerMetadata | IssuerMetadataV1_0_08, opts?: { prefLocales: string[] }): MetadataDisplay[] { +export function getIssuerDisplays(metadata: CredentialIssuerMetadata, opts?: { prefLocales: string[] }): MetadataDisplay[] { const matchedDisplays = metadata.display?.filter( (item) => !opts?.prefLocales || opts.prefLocales.length === 0 || (item.locale && opts.prefLocales.includes(item.locale)) || !item.locale, diff --git a/packages/common/lib/types/Authorization.types.ts b/packages/common/lib/types/Authorization.types.ts index 239b0e26..ad269967 100644 --- a/packages/common/lib/types/Authorization.types.ts +++ b/packages/common/lib/types/Authorization.types.ts @@ -219,6 +219,7 @@ export interface OpenIDResponse { origResponse: Response; successBody?: T; errorBody?: ErrorResponse; + selectedHost?: string; } export interface AccessTokenResponse { diff --git a/packages/common/lib/types/Generic.types.ts b/packages/common/lib/types/Generic.types.ts index b4e1fa40..f7bafa2c 100644 --- a/packages/common/lib/types/Generic.types.ts +++ b/packages/common/lib/types/Generic.types.ts @@ -99,10 +99,20 @@ export type CommonCredentialSupported = CredentialSupportedBrief & { display?: CredentialsSupportedDisplay[]; }; -export interface CredentialSupportedJwtVcJsonLdAndLdpVc extends CommonCredentialSupported { - types: string[]; // REQUIRED. JSON array designating the types a certain credential type supports + +export interface JwtVcCredentialDefinition { + type: string[]; // REQUIRED. JSON array designating the types a certain credential type supports + credentialSubject?: IssuerCredentialSubject; // OPTIONAL. A JSON object containing a list of key value pairs, where the key identifies the claim offered in the Credential. The value MAY be a dictionary, which allows to represent the full (potentially deeply nested) structure of the verifiable credential to be issued. +} + +export interface JwtVcJsonLdAndLdpVcCredentialDefinition extends JwtVcCredentialDefinition { + type: string[]; // REQUIRED. JSON array designating the types a certain credential type supports '@context': ICredentialContextType[]; // REQUIRED. JSON array as defined in [VC_DATA], Section 4.1. credentialSubject?: IssuerCredentialSubject; // OPTIONAL. A JSON object containing a list of key value pairs, where the key identifies the claim offered in the Credential. The value MAY be a dictionary, which allows to represent the full (potentially deeply nested) structure of the verifiable credential to be issued. +} + +export interface CredentialSupportedJwtVcJsonLdAndLdpVc extends CommonCredentialSupported { + credential_definition: JwtVcJsonLdAndLdpVcCredentialDefinition; // REQUIRED. JSON object containing the detailed description of the credential type order?: string[]; //An array of claims.display.name values that lists them in the order they should be displayed by the Wallet. format: 'ldp_vc' | 'jwt_vc_json-ld'; } @@ -112,6 +122,7 @@ export interface JwtVcCredentialDefinition { credentialSubject?: IssuerCredentialSubject; // OPTIONAL. A JSON object containing a list of key value pairs, where the key identifies the claim offered in the Credential. The value MAY be a dictionary, which allows to represent the full (potentially deeply nested) structure of the verifiable credential to be issued. } + export interface CredentialSupportedJwtVcJson extends CommonCredentialSupported { credential_definition: JwtVcCredentialDefinition; // REQUIRED. JSON object containing the detailed description of the credential type order?: string[]; //An array of claims.display.name values that lists them in the order they should be displayed by the Wallet. diff --git a/packages/common/lib/types/OpenID4VCIVersions.types.ts b/packages/common/lib/types/OpenID4VCIVersions.types.ts index 2efb39d4..7b138cdb 100644 --- a/packages/common/lib/types/OpenID4VCIVersions.types.ts +++ b/packages/common/lib/types/OpenID4VCIVersions.types.ts @@ -1,7 +1,6 @@ export enum OpenId4VCIVersion { - VER_1_0_08 = 1008, - VER_1_0_09 = 1009, - VER_1_0_11 = 1011, + VER_1_0_12 = 1012, + VER_UNSUPPORTED = Number.MIN_VALUE + 1, VER_UNKNOWN = Number.MIN_VALUE, } diff --git a/packages/common/lib/types/ServerMetadata.ts b/packages/common/lib/types/ServerMetadata.ts index b2e01d3e..b957b47b 100644 --- a/packages/common/lib/types/ServerMetadata.ts +++ b/packages/common/lib/types/ServerMetadata.ts @@ -1,5 +1,4 @@ import { CredentialIssuerMetadata } from './Generic.types'; -import { IssuerMetadataV1_0_08 } from './v1_0_08.types'; export interface AuthorizationServerMetadata { issuer: string; @@ -66,7 +65,7 @@ export interface EndpointMetadata { issuer: string; token_endpoint: string; credential_endpoint: string; - authorization_server?: string; + authorization_servers?: string[]; authorization_endpoint?: string; // Can be undefined in pre-auth flow } export interface EndpointMetadataResult extends EndpointMetadata { @@ -74,5 +73,5 @@ export interface EndpointMetadataResult extends EndpointMetadata { // The values below should not end up in requests/responses directly, so they are using our normal CamelCase convention authorizationServerType: AuthorizationServerType; authorizationServerMetadata?: AuthorizationServerMetadata; - credentialIssuerMetadata?: Partial & (CredentialIssuerMetadata | IssuerMetadataV1_0_08); + credentialIssuerMetadata?: Partial & (CredentialIssuerMetadata); } diff --git a/packages/common/lib/types/index.ts b/packages/common/lib/types/index.ts index 17e75b1b..133598d7 100644 --- a/packages/common/lib/types/index.ts +++ b/packages/common/lib/types/index.ts @@ -1,9 +1,7 @@ export * from './Authorization.types'; export * from './CredentialIssuance.types'; export * from './Generic.types'; -export * from './v1_0_08.types'; -export * from './v1_0_09.types'; -export * from './v1_0_11.types'; +export * from './v1_0_12.types'; export * from './ServerMetadata'; export * from './OpenID4VCIErrors'; export * from './OpenID4VCIVersions.types'; diff --git a/packages/common/lib/types/v1_0_08.types.ts b/packages/common/lib/types/v1_0_08.types.ts deleted file mode 100644 index b53c3921..00000000 --- a/packages/common/lib/types/v1_0_08.types.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { CredentialFormat } from '@sphereon/ssi-types'; - -import { ProofOfPossession } from './CredentialIssuance.types'; -import { CredentialsSupportedDisplay, CredentialSupportedBrief, IssuerCredentialSubject, MetadataDisplay, NameAndLocale } from './Generic.types'; - -export interface CredentialRequestV1_0_08 { - type: string; - format: CredentialFormat; - proof?: ProofOfPossession; -} - -export interface IssuerMetadataV1_0_08 { - issuer?: string; - credential_endpoint: string; // REQUIRED. URL of the OP's Credential Endpoint. This URL MUST use the https scheme and MAY contain port, path and query parameter components. - credentials_supported: CredentialSupportedTypeV1_0_08; // REQUIRED. A JSON object containing a list of key value pairs, where the key is a string serving as an abstract identifier of the Credential. This identifier is RECOMMENDED to be collision resistant - it can be globally unique, but does not have to be when naming conflicts are unlikely to arise in a given use case. The value is a JSON object. The JSON object MUST conform to the structure of the Section 11.2.1. - credential_issuer?: { - // OPTIONAL. A JSON object containing display properties for the Credential issuer. - display: NameAndLocale | NameAndLocale[]; // OPTIONAL. An array of objects, where each object contains display properties of a Credential issuer for a certain language. Below is a non-exhaustive list of valid parameters that MAY be included: - }; - authorization_servers?: string; - token_endpoint?: string; - display?: MetadataDisplay[]; - [x: string]: unknown; -} - -export interface CredentialOfferPayloadV1_0_08 { - issuer: string; //(url) REQUIRED The issuer URL of the Credential issuer, the Wallet is requested to obtain one or more Credentials from. - credential_type: string[] | string; //(url) REQUIRED A JSON string denoting the type of the Credential the Wallet shall request - 'pre-authorized_code'?: string; //CONDITIONAL the code representing the issuer's authorization for the Wallet to obtain Credentials of a certain type. This code MUST be short-lived and single-use. MUST be present in a pre-authorized code flow. - user_pin_required?: boolean | string; //OPTIONAL Boolean value specifying whether the issuer expects presentation of a user PIN along with the Token Request in a pre-authorized code flow. Default is false. - op_state?: string; //(JWT) OPTIONAL String value created by the Credential Issuer and opaque to the Wallet that is used to bind the subsequent authentication request with the Credential Issuer to a context set up during previous steps -} -export interface CredentialSupportedTypeV1_0_08 { - [credentialType: string]: CredentialSupportedV1_0_08; -} - -export interface CredentialSupportedFormatV1_0_08 extends CredentialSupportedBrief { - name?: string; - types: string[]; -} - -export interface CredentialSupportedV1_0_08 { - display?: CredentialsSupportedDisplay[]; - formats: { - // REQUIRED. A JSON object containing a list of key value pairs, where the key is a string identifying the format of the Credential. Below is a non-exhaustive list of valid key values defined by this specification: - [credentialFormat: string]: CredentialSupportedFormatV1_0_08; - }; - claims?: IssuerCredentialSubject; // REQUIRED. A JSON object containing a list of key value pairs, where the key identifies the claim offered in the Credential. The value is a JSON object detailing the specifics about the support for the claim with a following non-exhaustive list of parameters that MAY be included: -} diff --git a/packages/common/lib/types/v1_0_09.types.ts b/packages/common/lib/types/v1_0_09.types.ts deleted file mode 100644 index e45bf380..00000000 --- a/packages/common/lib/types/v1_0_09.types.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { CommonAuthorizationRequest } from './Authorization.types'; -import { CredentialOfferFormat } from './Generic.types'; - -export interface CredentialOfferV1_0_09 { - credential_offer: CredentialOfferPayloadV1_0_09; -} - -// https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-09.html#name-issuance-initiation-request -export interface CredentialOfferPayloadV1_0_09 { - /** - * REQUIRED. The URL of the Credential Issuer, the Wallet is requested to obtain one or more Credentials from. - */ - issuer: string; - /** - * REQUIRED. A JSON array, where every entry is a JSON object or a JSON string. If the entry is an object, - * the object contains the data related to a certain credential type the Wallet MAY request. - * Each object MUST contain a format Claim determining the format of the credential to be requested and - * further parameters characterising the type of the credential to be requested as defined in Appendix E. - * If the entry is a string, the string value MUST be one of the id values in one of the objects in the - * credentials_supported Credential Issuer metadata parameter. - * When processing, the Wallet MUST resolve this string value to the respective object. - */ - credentials: (CredentialOfferFormat | string)[]; - 'pre-authorized_code'?: string; //CONDITIONAL the code representing the issuer's authorization for the Wallet to obtain Credentials of a certain type. This code MUST be short-lived and single-use. MUST be present in a pre-authorized code flow. - user_pin_required?: boolean | string; //OPTIONAL Boolean value specifying whether the issuer expects presentation of a user PIN along with the Token Request in a pre-authorized code flow. Default is false. - op_state?: string; //(JWT) OPTIONAL String value created by the Credential Issuer and opaque to the Wallet that is used to bind the subsequent authentication request with the Credential Issuer to a context set up during previous steps -} - -export interface AuthorizationRequestV1_0_09 extends CommonAuthorizationRequest { - op_state?: string; -} - -// todo https://sphereon.atlassian.net/browse/VDX-185 -export function isAuthorizationRequestV1_0_09(request: CommonAuthorizationRequest): boolean { - return request && 'op_state' in request; -} diff --git a/packages/issuer-rest/lib/OID4VCIServer.ts b/packages/issuer-rest/lib/OID4VCIServer.ts index bdc17189..732d026e 100644 --- a/packages/issuer-rest/lib/OID4VCIServer.ts +++ b/packages/issuer-rest/lib/OID4VCIServer.ts @@ -8,7 +8,7 @@ import { OID4VCICredentialFormat, QRCodeOpts, } from '@sphereon/oid4vci-common' -import { CredentialSupportedBuilderV1_11, ITokenEndpointOpts, VcIssuer, VcIssuerBuilder } from '@sphereon/oid4vci-issuer' +import { CredentialSupportedBuilderV1_12, ITokenEndpointOpts, VcIssuer, VcIssuerBuilder } from '@sphereon/oid4vci-issuer' import { ExpressSupport, HasEndpointOpts, ISingleEndpointOpts } from '@sphereon/ssi-express-support' import express, { Express } from 'express' @@ -24,7 +24,7 @@ import { } from './oid4vci-api-functions' function buildVCIFromEnvironment() { - const credentialsSupported: CredentialSupported = new CredentialSupportedBuilderV1_11() + const credentialsSupported: CredentialSupported = new CredentialSupportedBuilderV1_12() .withCryptographicSuitesSupported(process.env.cryptographic_suites_supported as string) .withCryptographicBindingMethod(process.env.cryptographic_binding_methods_supported as string) .withFormat(process.env.credential_supported_format as unknown as OID4VCICredentialFormat) @@ -50,7 +50,9 @@ function buildVCIFromEnvironment() { .build() return new VcIssuerBuilder() .withUserPinRequired(process.env.user_pin_required as unknown as boolean) - .withAuthorizationServer(process.env.authorization_server as string) + .withAuthorizationServers(process.env.authorization_servers + ? (process.env.authorization_servers as string).split(',') + : [process.env.authorization_server as string]) .withCredentialEndpoint(process.env.credential_endpoint as string) .withCredentialIssuer(process.env.credential_issuer as string) .withIssuerDisplay({ diff --git a/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts b/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts index c19316be..7812cdae 100644 --- a/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts +++ b/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts @@ -14,7 +14,7 @@ import { OpenId4VCIVersion, } from '@sphereon/oid4vci-common' import { VcIssuer } from '@sphereon/oid4vci-issuer/dist/VcIssuer' -import { CredentialSupportedBuilderV1_11, VcIssuerBuilder } from '@sphereon/oid4vci-issuer/dist/builder' +import { CredentialSupportedBuilderV1_12, VcIssuerBuilder } from '@sphereon/oid4vci-issuer/dist/builder' import { MemoryStates } from '@sphereon/oid4vci-issuer/dist/state-manager' import { ExpressBuilder, ExpressSupport } from '@sphereon/ssi-express-support' import { IProofPurpose, IProofType } from '@sphereon/ssi-types' @@ -70,7 +70,7 @@ describe('VcIssuer', () => { return new jose.SignJWT({ ...jwt.payload }).setProtectedHeader({ ...jwt.header, alg: Alg.ES256 }).sign(privateKey) } - const credentialsSupported: CredentialSupported = new CredentialSupportedBuilderV1_11() + const credentialsSupported: CredentialSupported = new CredentialSupportedBuilderV1_12() .withCryptographicSuitesSupported('ES256K') .withCryptographicBindingMethod('did') .withTypes('VerifiableCredential') @@ -245,7 +245,7 @@ describe('VcIssuer', () => { version: 1011, }) expect(client.getIssuer()).toEqual(ISSUER_URL) - expect(client.version()).toEqual(OpenId4VCIVersion.VER_1_0_11) + expect(client.version()).toEqual(OpenId4VCIVersion.VER_1_0_12) }) it('should retrieve server metadata', async () => { diff --git a/packages/issuer-rest/lib/oid4vci-api-functions.ts b/packages/issuer-rest/lib/oid4vci-api-functions.ts index a595f259..3849d1e8 100644 --- a/packages/issuer-rest/lib/oid4vci-api-functions.ts +++ b/packages/issuer-rest/lib/oid4vci-api-functions.ts @@ -5,7 +5,7 @@ import { ACCESS_TOKEN_ISSUER_REQUIRED_ERROR, AuthorizationRequest, CredentialOfferRESTRequest, - CredentialRequestV1_0_11, + CredentialRequestV1_0_12, determineGrantTypes, getNumberOrUndefined, Grant, @@ -141,7 +141,7 @@ export function getCredentialEndpoint( console.log(`[OID4VCI] getCredential endpoint enabled at ${path}`) router.post(path, async (request: Request, response: Response) => { try { - const credentialRequest = request.body as CredentialRequestV1_0_11 + const credentialRequest = request.body as CredentialRequestV1_0_12 const credential = await issuer.issueCredential({ credentialRequest: credentialRequest, tokenExpiresIn: opts.tokenExpiresIn, diff --git a/packages/issuer/lib/VcIssuer.ts b/packages/issuer/lib/VcIssuer.ts index e8d53139..fe084bb7 100644 --- a/packages/issuer/lib/VcIssuer.ts +++ b/packages/issuer/lib/VcIssuer.ts @@ -8,10 +8,9 @@ import { CredentialDataSupplierInput, CredentialIssuerMetadataOpts, CredentialOfferFormat, - CredentialOfferPayloadV1_0_11, + CredentialOfferPayloadV1_0_12, CredentialOfferSession, - CredentialOfferV1_0_12, - CredentialRequestV1_0_11, + CredentialOfferV1_0_12, CredentialRequestV1_0_12, CredentialResponse, DID_NO_DIDDOC_ERROR, Grant, @@ -33,8 +32,8 @@ import { toUniformCredentialOfferRequest, TYP_ERROR, UniformCredentialRequest, - URIState, -} from '@sphereon/oid4vci-common' + URIState +} from '@sphereon/oid4vci-common'; import { ICredential, W3CVerifiableCredential } from '@sphereon/ssi-types' import { v4 } from 'uuid' @@ -109,12 +108,12 @@ export class VcIssuer { if (!grants?.authorization_code && !grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']) { throw Error(`No grant issuer state or pre-authorized code could be deduced`) } - const credentialOfferPayload: CredentialOfferPayloadV1_0_11 = { + const credentialOfferPayload: CredentialOfferPayloadV1_0_12 = { ...(grants && { grants }), ...(credentials && { credentials }), ...(credentialDefinition && { credential_definition: credentialDefinition }), credential_issuer: this.issuerMetadata.credential_issuer, - } as CredentialOfferPayloadV1_0_11 + } as CredentialOfferPayloadV1_0_12 if (grants?.authorization_code) { issuerState = grants?.authorization_code.issuer_state if (!issuerState) { @@ -174,9 +173,9 @@ export class VcIssuer { { credential_offer: credentialOfferObject.credential_offer, credential_offer_uri: credentialOfferObject.credential_offer_uri, - } as CredentialOfferV1_0_11, + } as CredentialOfferV1_0_12, { - version: OpenId4VCIVersion.VER_1_0_11, + version: OpenId4VCIVersion.VER_1_0_12, resolve: false, // We are creating the object, so do not resolve }, ) @@ -229,7 +228,7 @@ export class VcIssuer { * - cNonce an existing c_nonce */ public async issueCredential(opts: { - credentialRequest: CredentialRequestV1_0_11 + credentialRequest: CredentialRequestV1_0_12 credential?: ICredential credentialDataSupplier?: CredentialDataSupplier credentialDataSupplierInput?: CredentialDataSupplierInput diff --git a/packages/issuer/lib/__tests__/CredentialOfferUtils.spec.ts b/packages/issuer/lib/__tests__/CredentialOfferUtils.spec.ts index ed5b4fe0..2079c246 100644 --- a/packages/issuer/lib/__tests__/CredentialOfferUtils.spec.ts +++ b/packages/issuer/lib/__tests__/CredentialOfferUtils.spec.ts @@ -1,4 +1,4 @@ -import { CredentialOfferPayloadV1_0_11 } from '@sphereon/oid4vci-common' +import { CredentialOfferPayloadV1_0_12 } from '@sphereon/oid4vci-common' import { createCredentialOfferURI } from '../index' @@ -19,7 +19,7 @@ describe('CredentialOfferUtils should', () => { issuer_state: 'eyJhbGciOiJSU0Et...FYUaBy', }, }, - } as CredentialOfferPayloadV1_0_11 + } as CredentialOfferPayloadV1_0_12 expect(createCredentialOfferURI(undefined, { credentialOffer, state: 'eyJhbGciOiJSU0Et...FYUaBy' })).toEqual( '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', ) diff --git a/packages/issuer/lib/__tests__/VcIssuer.spec.ts b/packages/issuer/lib/__tests__/VcIssuer.spec.ts index 98af5fb4..c5e56954 100644 --- a/packages/issuer/lib/__tests__/VcIssuer.spec.ts +++ b/packages/issuer/lib/__tests__/VcIssuer.spec.ts @@ -11,7 +11,7 @@ import { IProofPurpose, IProofType } from '@sphereon/ssi-types' import { DIDDocument } from 'did-resolver' import { VcIssuer } from '../VcIssuer' -import { CredentialSupportedBuilderV1_11, VcIssuerBuilder } from '../builder' +import { CredentialSupportedBuilderV1_12, VcIssuerBuilder } from '../builder' import { MemoryStates } from '../state-manager' const IDENTIPROOF_ISSUER_URL = 'https://issuer.research.identiproof.io' @@ -24,7 +24,7 @@ describe('VcIssuer', () => { beforeAll(async () => { jest.clearAllMocks() - const credentialsSupported: CredentialSupported = new CredentialSupportedBuilderV1_11() + const credentialsSupported: CredentialSupported = new CredentialSupportedBuilderV1_12() .withCryptographicSuitesSupported('ES256K') .withCryptographicBindingMethod('did') .withFormat('jwt_vc_json') @@ -78,7 +78,7 @@ describe('VcIssuer', () => { }, }) vcIssuer = new VcIssuerBuilder() - .withAuthorizationServer('https://authorization-server') + .withAuthorizationServers('https://authorization-server') .withCredentialEndpoint('https://credential-endpoint') .withCredentialIssuer(IDENTIPROOF_ISSUER_URL) .withIssuerDisplay({ diff --git a/packages/issuer/lib/__tests__/VcIssuerBuilder.spec.ts b/packages/issuer/lib/__tests__/VcIssuerBuilder.spec.ts index c5f64b04..411484ee 100644 --- a/packages/issuer/lib/__tests__/VcIssuerBuilder.spec.ts +++ b/packages/issuer/lib/__tests__/VcIssuerBuilder.spec.ts @@ -1,11 +1,11 @@ import { CredentialSupported, IssuerCredentialSubjectDisplay, IssueStatus, TokenErrorResponse } from '@sphereon/oid4vci-common' import { v4 } from 'uuid' -import { CredentialSupportedBuilderV1_11, VcIssuerBuilder } from '../index' +import { CredentialSupportedBuilderV1_12, VcIssuerBuilder } from '../index' describe('VcIssuer builder should', () => { it('generate a VcIssuer', () => { - const credentialsSupported: CredentialSupported = new CredentialSupportedBuilderV1_11() + const credentialsSupported: CredentialSupported = new CredentialSupportedBuilderV1_12() .withCryptographicSuitesSupported('ES256K') .withCryptographicBindingMethod('did') .withFormat('jwt_vc_json') @@ -27,7 +27,7 @@ describe('VcIssuer builder should', () => { } as IssuerCredentialSubjectDisplay) .build() const vcIssuer = new VcIssuerBuilder() - .withAuthorizationServer('https://authorization-server') + .withAuthorizationServers('https://authorization-server') .withCredentialEndpoint('https://credential-endpoint') .withCredentialIssuer('https://credential-issuer') .withIssuerDisplay({ @@ -45,7 +45,7 @@ describe('VcIssuer builder should', () => { }) it('fail to generate a VcIssuer', () => { - const credentialsSupported: CredentialSupported = new CredentialSupportedBuilderV1_11() + const credentialsSupported: CredentialSupported = new CredentialSupportedBuilderV1_12() .withCryptographicSuitesSupported('ES256K') .withCryptographicBindingMethod('did') .withFormat('jwt_vc_json') @@ -68,7 +68,7 @@ describe('VcIssuer builder should', () => { .build() expect(() => new VcIssuerBuilder() - .withAuthorizationServer('https://authorization-server') + .withAuthorizationServers('https://authorization-server') .withCredentialEndpoint('https://credential-endpoint') .withIssuerDisplay({ name: 'example issuer', @@ -81,7 +81,7 @@ describe('VcIssuer builder should', () => { it('fail to generate a CredentialSupportedV1_11', () => { expect(() => - new CredentialSupportedBuilderV1_11() + new CredentialSupportedBuilderV1_12() .withCryptographicSuitesSupported('ES256K') .withCryptographicBindingMethod('did') .withId('UniversityDegree_JWT') @@ -89,7 +89,7 @@ describe('VcIssuer builder should', () => { ).toThrowError(TokenErrorResponse.invalid_request) }) it('should successfully attach an instance of the ICredentialOfferStateManager to the VcIssuer instance', async () => { - const credentialsSupported: CredentialSupported = new CredentialSupportedBuilderV1_11() + const credentialsSupported: CredentialSupported = new CredentialSupportedBuilderV1_12() .withCryptographicSuitesSupported('ES256K') .withCryptographicBindingMethod('did') .withFormat('jwt_vc_json') @@ -111,7 +111,7 @@ describe('VcIssuer builder should', () => { } as IssuerCredentialSubjectDisplay) .build() const vcIssuer = new VcIssuerBuilder() - .withAuthorizationServer('https://authorization-server') + .withAuthorizationServers('https://authorization-server') .withCredentialEndpoint('https://credential-endpoint') .withCredentialIssuer('https://credential-issuer') .withIssuerDisplay({ diff --git a/packages/issuer/lib/builder/CredentialSupportedBuilderV1_11.ts b/packages/issuer/lib/builder/CredentialSupportedBuilderV1_12.ts similarity index 89% rename from packages/issuer/lib/builder/CredentialSupportedBuilderV1_11.ts rename to packages/issuer/lib/builder/CredentialSupportedBuilderV1_12.ts index 1ba45598..ac5e16ee 100644 --- a/packages/issuer/lib/builder/CredentialSupportedBuilderV1_11.ts +++ b/packages/issuer/lib/builder/CredentialSupportedBuilderV1_12.ts @@ -2,14 +2,13 @@ import { CredentialsSupportedDisplay, CredentialSupported, isFormat, - isNotFormat, IssuerCredentialSubject, IssuerCredentialSubjectDisplay, OID4VCICredentialFormat, TokenErrorResponse, } from '@sphereon/oid4vci-common' -export class CredentialSupportedBuilderV1_11 { +export class CredentialSupportedBuilderV1_12 { format?: OID4VCICredentialFormat id?: string types?: string[] @@ -18,17 +17,17 @@ export class CredentialSupportedBuilderV1_11 { display?: CredentialsSupportedDisplay[] credentialSubject?: IssuerCredentialSubject - withFormat(credentialFormat: OID4VCICredentialFormat): CredentialSupportedBuilderV1_11 { + withFormat(credentialFormat: OID4VCICredentialFormat): CredentialSupportedBuilderV1_12 { this.format = credentialFormat return this } - withId(id: string): CredentialSupportedBuilderV1_11 { + withId(id: string): CredentialSupportedBuilderV1_12 { this.id = id return this } - addTypes(type: string | string[]): CredentialSupportedBuilderV1_11 { + addTypes(type: string | string[]): CredentialSupportedBuilderV1_12 { if (!Array.isArray(type)) { this.types = this.types ? [...this.types, type] : [type] } else { @@ -39,7 +38,7 @@ export class CredentialSupportedBuilderV1_11 { return this } - withTypes(type: string | string[]): CredentialSupportedBuilderV1_11 { + withTypes(type: string | string[]): CredentialSupportedBuilderV1_12 { if (this.format === 'vc+sd-jwt' && Array.isArray(type) && type.length > 1) { throw new Error('Only one type is allowed for vc+sd-jwt') } @@ -47,7 +46,7 @@ export class CredentialSupportedBuilderV1_11 { return this } - addCryptographicBindingMethod(method: string | string[]): CredentialSupportedBuilderV1_11 { + addCryptographicBindingMethod(method: string | string[]): CredentialSupportedBuilderV1_12 { if (!Array.isArray(method)) { this.cryptographicBindingMethodsSupported = this.cryptographicBindingMethodsSupported ? [...this.cryptographicBindingMethodsSupported, method] @@ -60,12 +59,12 @@ export class CredentialSupportedBuilderV1_11 { return this } - withCryptographicBindingMethod(method: string | string[]): CredentialSupportedBuilderV1_11 { + withCryptographicBindingMethod(method: string | string[]): CredentialSupportedBuilderV1_12 { this.cryptographicBindingMethodsSupported = Array.isArray(method) ? method : [method] return this } - addCryptographicSuitesSupported(suit: string | string[]): CredentialSupportedBuilderV1_11 { + addCryptographicSuitesSupported(suit: string | string[]): CredentialSupportedBuilderV1_12 { if (!Array.isArray(suit)) { this.cryptographicSuitesSupported = this.cryptographicSuitesSupported ? [...this.cryptographicSuitesSupported, suit] : [suit] } else { @@ -74,12 +73,12 @@ export class CredentialSupportedBuilderV1_11 { return this } - withCryptographicSuitesSupported(suit: string | string[]): CredentialSupportedBuilderV1_11 { + withCryptographicSuitesSupported(suit: string | string[]): CredentialSupportedBuilderV1_12 { this.cryptographicSuitesSupported = Array.isArray(suit) ? suit : [suit] return this } - addCredentialSupportedDisplay(credentialDisplay: CredentialsSupportedDisplay | CredentialsSupportedDisplay[]): CredentialSupportedBuilderV1_11 { + addCredentialSupportedDisplay(credentialDisplay: CredentialsSupportedDisplay | CredentialsSupportedDisplay[]): CredentialSupportedBuilderV1_12 { if (!Array.isArray(credentialDisplay)) { this.display = this.display ? [...this.display, credentialDisplay] : [credentialDisplay] } else { @@ -88,7 +87,7 @@ export class CredentialSupportedBuilderV1_11 { return this } - withCredentialSupportedDisplay(credentialDisplay: CredentialsSupportedDisplay | CredentialsSupportedDisplay[]): CredentialSupportedBuilderV1_11 { + withCredentialSupportedDisplay(credentialDisplay: CredentialsSupportedDisplay | CredentialsSupportedDisplay[]): CredentialSupportedBuilderV1_12 { this.display = Array.isArray(credentialDisplay) ? credentialDisplay : [credentialDisplay] return this } @@ -101,7 +100,7 @@ export class CredentialSupportedBuilderV1_11 { addCredentialSubjectPropertyDisplay( subjectProperty: string, issuerCredentialSubjectDisplay: IssuerCredentialSubjectDisplay, - ): CredentialSupportedBuilderV1_11 { + ): CredentialSupportedBuilderV1_12 { if (!this.credentialSubject) { this.credentialSubject = {} } @@ -130,11 +129,10 @@ export class CredentialSupportedBuilderV1_11 { } } // And else would work here, but this way we get the correct typing - else if (isNotFormat(credentialSupported, 'vc+sd-jwt')) { - credentialSupported.types = this.types - - if (this.credentialSubject) { - credentialSupported.credentialSubject = this.credentialSubject + else { + credentialSupported.credential_definition = { + type: this.types, + ...this.credentialSubject ? { credentialSubject: this.credentialSubject } : {} } } diff --git a/packages/issuer/lib/builder/IssuerMetadataBuilderV1_11.ts b/packages/issuer/lib/builder/IssuerMetadataBuilderV1_11.ts index 30f28060..b14258c8 100644 --- a/packages/issuer/lib/builder/IssuerMetadataBuilderV1_11.ts +++ b/packages/issuer/lib/builder/IssuerMetadataBuilderV1_11.ts @@ -1,12 +1,12 @@ import { CredentialIssuerMetadata, CredentialSupported, MetadataDisplay } from '@sphereon/oid4vci-common' -import { CredentialSupportedBuilderV1_11 } from './CredentialSupportedBuilderV1_11' +import { CredentialSupportedBuilderV1_12 } from './CredentialSupportedBuilderV1_12' import { DisplayBuilder } from './DisplayBuilder' export class IssuerMetadataBuilderV1_11 { credentialEndpoint: string | undefined credentialIssuer: string | undefined - supportedBuilders: CredentialSupportedBuilderV1_11[] = [] + supportedBuilders: CredentialSupportedBuilderV1_12[] = [] supportedCredentials: CredentialSupported[] = [] displayBuilders: DisplayBuilder[] = [] display: MetadataDisplay[] = [] @@ -39,13 +39,13 @@ export class IssuerMetadataBuilderV1_11 { return this } - public newSupportedCredentialBuilder(): CredentialSupportedBuilderV1_11 { - const builder = new CredentialSupportedBuilderV1_11() + public newSupportedCredentialBuilder(): CredentialSupportedBuilderV1_12 { + const builder = new CredentialSupportedBuilderV1_12() this.addSupportedCredentialBuilder(builder) return builder } - public addSupportedCredentialBuilder(supportedCredentialBuilder: CredentialSupportedBuilderV1_11) { + public addSupportedCredentialBuilder(supportedCredentialBuilder: CredentialSupportedBuilderV1_12) { this.supportedBuilders.push(supportedCredentialBuilder) return this } diff --git a/packages/issuer/lib/builder/VcIssuerBuilder.ts b/packages/issuer/lib/builder/VcIssuerBuilder.ts index dd058a87..42215467 100644 --- a/packages/issuer/lib/builder/VcIssuerBuilder.ts +++ b/packages/issuer/lib/builder/VcIssuerBuilder.ts @@ -49,8 +49,8 @@ export class VcIssuerBuilder { return this } - public withAuthorizationServer(authorizationServer: string): this { - this.issuerMetadata.authorization_servers = authorizationServer + public withAuthorizationServers(authorizationServers: string[]): this { + this.issuerMetadata.authorization_servers = authorizationServers return this } diff --git a/packages/issuer/lib/builder/index.ts b/packages/issuer/lib/builder/index.ts index 1918c288..f387f30d 100644 --- a/packages/issuer/lib/builder/index.ts +++ b/packages/issuer/lib/builder/index.ts @@ -1,4 +1,4 @@ -export * from './CredentialSupportedBuilderV1_11' +export * from './CredentialSupportedBuilderV1_12' export * from './VcIssuerBuilder' export * from './IssuerMetadataBuilderV1_11' export * from './DisplayBuilder' diff --git a/packages/issuer/lib/functions/CredentialOfferUtils.ts b/packages/issuer/lib/functions/CredentialOfferUtils.ts index 86235620..7e795831 100644 --- a/packages/issuer/lib/functions/CredentialOfferUtils.ts +++ b/packages/issuer/lib/functions/CredentialOfferUtils.ts @@ -1,7 +1,7 @@ import { CredentialIssuerMetadata, CredentialIssuerMetadataOpts, - CredentialOfferPayloadV1_0_11, + CredentialOfferPayloadV1_0_12, CredentialOfferSession, CredentialOfferV1_0_12, Grant, @@ -14,7 +14,7 @@ export function createCredentialOfferObject( issuerMetadata?: CredentialIssuerMetadataOpts, // todo: probably it's wise to create another builder for CredentialOfferPayload that will generate different kinds of CredentialOfferPayload opts?: { - credentialOffer?: CredentialOfferPayloadV1_0_11 + credentialOffer?: CredentialOfferPayloadV1_0_12 credentialOfferUri?: string scheme?: string baseUri?: string @@ -22,7 +22,7 @@ export function createCredentialOfferObject( preAuthorizedCode?: string userPinRequired?: boolean }, -): CredentialOfferV1_0_11 & { scheme: string; grants: Grant; baseUri: string } { +): CredentialOfferV1_0_12 & { scheme: string; grants: Grant; baseUri: string } { if (!issuerMetadata && !opts?.credentialOffer && !opts?.credentialOfferUri) { throw new Error('You have to provide issuerMetadata or credentialOffer object for creating a deeplink') } @@ -45,7 +45,7 @@ export function createCredentialOfferObject( baseUri = baseUri.replace(`${scheme}://`, '') const credential_offer_uri = opts?.credentialOfferUri ? `${scheme}://${baseUri}?credential_offer_uri=${opts?.credentialOfferUri}` : undefined - let credential_offer: CredentialOfferPayloadV1_0_11 + let credential_offer: CredentialOfferPayloadV1_0_12 if (opts?.credentialOffer) { credential_offer = { ...opts.credentialOffer, @@ -55,7 +55,7 @@ export function createCredentialOfferObject( credential_offer = { credential_issuer: issuerMetadata?.credential_issuer, credentials: issuerMetadata?.credentials_supported, - } as CredentialOfferPayloadV1_0_11 + } as CredentialOfferPayloadV1_0_12 } // todo: check payload against issuer metadata. Especially strings in the credentials array: When processing, the Wallet MUST resolve this string value to the respective object. @@ -78,7 +78,7 @@ export function createCredentialOfferObject( } export function createCredentialOfferURIFromObject( - credentialOffer: (CredentialOfferV1_0_11 | UniformCredentialOffer) & { scheme?: string; baseUri?: string; grant?: Grant }, + credentialOffer: (CredentialOfferV1_0_12 | UniformCredentialOffer) & { scheme?: string; baseUri?: string; grant?: Grant }, opts?: { scheme?: string; baseUri?: string }, ) { const scheme = opts?.scheme?.replace('://', '') ?? credentialOffer?.scheme?.replace('://', '') ?? 'openid-credential-offer' @@ -104,7 +104,7 @@ export function createCredentialOfferURI( // todo: probably it's wise to create another builder for CredentialOfferPayload that will generate different kinds of CredentialOfferPayload opts?: { state?: string - credentialOffer?: CredentialOfferPayloadV1_0_11 + credentialOffer?: CredentialOfferPayloadV1_0_12 credentialOfferUri?: string scheme?: string preAuthorizedCode?: string