From 6556cc0bf7c8cb11907684fbecb6d9464e0e53f2 Mon Sep 17 00:00:00 2001 From: Niels Klomp Date: Mon, 12 Aug 2024 11:28:56 +0200 Subject: [PATCH] feat: Add support for mDL / mdoc to the OID4VCI client --- .../client/lib/AuthorizationCodeClient.ts | 4 +- .../lib/AuthorizationCodeClientV1_0_11.ts | 4 +- .../client/lib/CredentialRequestClient.ts | 11 +++++- .../lib/CredentialRequestClientV1_0_11.ts | 10 +++++ packages/client/lib/MetadataClient.ts | 3 +- packages/client/lib/MetadataClientV1_0_13.ts | 3 +- packages/client/lib/OpenID4VCIClient.ts | 2 +- .../client/lib/OpenID4VCIClientV1_0_11.ts | 2 +- .../client/lib/OpenID4VCIClientV1_0_13.ts | 2 +- .../lib/functions/CredentialRequestUtil.ts | 2 + .../lib/functions/FormatUtils.ts | 2 +- .../oid4vci-common/lib/functions/ProofUtil.ts | 3 +- .../lib/functions/TypeConversionUtils.ts | 28 +++++++++++--- .../lib/types/Authorization.types.ts | 20 +++++++++- .../lib/types/CredentialIssuance.types.ts | 22 +++++------ .../oid4vci-common/lib/types/Generic.types.ts | 38 ++++++++++++++++--- .../oid4vci-common/lib/types/v1_0_09.types.ts | 4 +- .../oid4vci-common/lib/types/v1_0_11.types.ts | 4 +- .../oid4vci-common/lib/types/v1_0_13.types.ts | 12 ++++++ 19 files changed, 137 insertions(+), 39 deletions(-) diff --git a/packages/client/lib/AuthorizationCodeClient.ts b/packages/client/lib/AuthorizationCodeClient.ts index 80817660..7aaa7b15 100644 --- a/packages/client/lib/AuthorizationCodeClient.ts +++ b/packages/client/lib/AuthorizationCodeClient.ts @@ -148,8 +148,9 @@ export const createAuthorizationRequestUrl = async ({ // SD-JWT VC const vct = cred.format === 'vc+sd-jwt' ? cred.vct : undefined; + const doctype = cred.format === 'mso_mdoc' ? cred.doctype : undefined; - // W3C credentials + // W3C credentials have a credential definition, the rest does not let credential_definition: undefined | Partial = undefined; if (isW3cCredentialSupported(cred)) { @@ -171,6 +172,7 @@ export const createAuthorizationRequestUrl = async ({ ...(credential_configuration_id && { credential_configuration_id }), ...(format && { format }), ...(vct && { vct, claims: cred.claims ? removeDisplayAndValueTypes(cred.claims) : undefined }), + ...(doctype && { doctype, claims: cred.claims ? removeDisplayAndValueTypes(cred.claims) : undefined }), } as AuthorizationDetails; }); if (!authorizationDetails || authorizationDetails.length === 0) { diff --git a/packages/client/lib/AuthorizationCodeClientV1_0_11.ts b/packages/client/lib/AuthorizationCodeClientV1_0_11.ts index f5aba26a..afef7394 100644 --- a/packages/client/lib/AuthorizationCodeClientV1_0_11.ts +++ b/packages/client/lib/AuthorizationCodeClientV1_0_11.ts @@ -4,7 +4,7 @@ import { CodeChallengeMethod, convertJsonToURI, CreateRequestObjectMode, - CredentialOfferFormat, + CredentialOfferFormatV1_0_11, CredentialOfferPayloadV1_0_11, CredentialOfferRequestWithBaseUrl, CredentialsSupportedLegacy, @@ -47,7 +47,7 @@ export const createAuthorizationRequestUrlV1_0_11 = async ({ if (!credentialOffer) { throw Error('Please provide a scope or authorization_details if no credential offer is present'); } - const creds: (CredentialOfferFormat | string)[] = (credentialOffer.credential_offer as CredentialOfferPayloadV1_0_11).credentials; + const creds: (CredentialOfferFormatV1_0_11 | string)[] = (credentialOffer.credential_offer as CredentialOfferPayloadV1_0_11).credentials; // FIXME: complains about VCT for sd-jwt // eslint-disable-next-line @typescript-eslint/ban-ts-comment diff --git a/packages/client/lib/CredentialRequestClient.ts b/packages/client/lib/CredentialRequestClient.ts index 9b53ccb8..42ea13b4 100644 --- a/packages/client/lib/CredentialRequestClient.ts +++ b/packages/client/lib/CredentialRequestClient.ts @@ -261,13 +261,22 @@ export class CredentialRequestClient { if (types.length > 1) { throw Error(`Only a single credential type is supported for ${format}`); } - // fixme: this isn't up to the CredentialRequest that we see in the version v1_0_13 return { format, proof, vct: types[0], ...opts.subjectIssuance, }; + } else if (format === 'mso_mdoc') { + if (types.length > 1) { + throw Error(`Only a single credential type is supported for ${format}`); + } + return { + format, + proof, + doctype: types[0], + ...opts.subjectIssuance, + }; } throw new Error(`Unsupported format: ${format}`); diff --git a/packages/client/lib/CredentialRequestClientV1_0_11.ts b/packages/client/lib/CredentialRequestClientV1_0_11.ts index 7fccd1ee..f1669aa0 100644 --- a/packages/client/lib/CredentialRequestClientV1_0_11.ts +++ b/packages/client/lib/CredentialRequestClientV1_0_11.ts @@ -215,6 +215,16 @@ export class CredentialRequestClientV1_0_11 { proof, vct: types[0], }; + } else if (format === 'mso_mdoc') { + if (types.length > 1) { + throw Error(`Only a single credential type is supported for ${format}`); + } + + return { + format, + proof, + doctype: types[0], + }; } throw new Error(`Unsupported format: ${format}`); diff --git a/packages/client/lib/MetadataClient.ts b/packages/client/lib/MetadataClient.ts index b9f2f394..90a59755 100644 --- a/packages/client/lib/MetadataClient.ts +++ b/packages/client/lib/MetadataClient.ts @@ -19,7 +19,7 @@ import Debug from 'debug'; import { MetadataClientV1_0_11 } from './MetadataClientV1_0_11'; import { MetadataClientV1_0_13 } from './MetadataClientV1_0_13'; -import { retrieveWellknown } from './functions/OpenIDUtils'; +import { retrieveWellknown } from './functions'; const debug = Debug('sphereon:oid4vci:metadata'); @@ -204,6 +204,7 @@ export class MetadataClient { * Retrieve only the OID4VCI metadata for the issuer. So no OIDC/OAuth2 metadata * * @param issuerHost The issuer hostname + * @param opts */ public static async retrieveOpenID4VCIServerMetadata( issuerHost: string, diff --git a/packages/client/lib/MetadataClientV1_0_13.ts b/packages/client/lib/MetadataClientV1_0_13.ts index b9076236..6318e6ec 100644 --- a/packages/client/lib/MetadataClientV1_0_13.ts +++ b/packages/client/lib/MetadataClientV1_0_13.ts @@ -12,7 +12,7 @@ import { } from '@sphereon/oid4vci-common'; import Debug from 'debug'; -import { retrieveWellknown } from './functions/OpenIDUtils'; +import { retrieveWellknown } from './functions'; const debug = Debug('sphereon:oid4vci:metadata'); @@ -174,6 +174,7 @@ export class MetadataClientV1_0_13 { * Retrieve only the OID4VCI metadata for the issuer. So no OIDC/OAuth2 metadata * * @param issuerHost The issuer hostname + * @param opts */ public static async retrieveOpenID4VCIServerMetadata( issuerHost: string, diff --git a/packages/client/lib/OpenID4VCIClient.ts b/packages/client/lib/OpenID4VCIClient.ts index 7827a0b3..0ec729d2 100644 --- a/packages/client/lib/OpenID4VCIClient.ts +++ b/packages/client/lib/OpenID4VCIClient.ts @@ -276,7 +276,7 @@ export class OpenID4VCIClient { authorizationResponse?: string | AuthorizationResponse; // Pass in an auth response, either as URI/redirect, or object additionalRequestParams?: Record; }, - ): Promise { + ): Promise { const { pin, clientId = this._state.clientId ?? this._state.authorizationRequestOpts?.clientId } = opts ?? {}; let { redirectUri } = opts ?? {}; if (opts?.authorizationResponse) { diff --git a/packages/client/lib/OpenID4VCIClientV1_0_11.ts b/packages/client/lib/OpenID4VCIClientV1_0_11.ts index 050369c0..4d06dece 100644 --- a/packages/client/lib/OpenID4VCIClientV1_0_11.ts +++ b/packages/client/lib/OpenID4VCIClientV1_0_11.ts @@ -262,7 +262,7 @@ export class OpenID4VCIClientV1_0_11 { authorizationResponse?: string | AuthorizationResponse; // Pass in an auth response, either as URI/redirect, or object additionalRequestParams?: Record; }, - ): Promise { + ): Promise { const { pin, clientId = this._state.clientId ?? this._state.authorizationRequestOpts?.clientId } = opts ?? {}; let { redirectUri } = opts ?? {}; if (opts?.authorizationResponse) { diff --git a/packages/client/lib/OpenID4VCIClientV1_0_13.ts b/packages/client/lib/OpenID4VCIClientV1_0_13.ts index dae99fda..b32ca85c 100644 --- a/packages/client/lib/OpenID4VCIClientV1_0_13.ts +++ b/packages/client/lib/OpenID4VCIClientV1_0_13.ts @@ -267,7 +267,7 @@ export class OpenID4VCIClientV1_0_13 { authorizationResponse?: string | AuthorizationResponse; // Pass in an auth response, either as URI/redirect, or object additionalRequestParams?: Record; }, - ): Promise { + ): Promise { const { pin, clientId = this._state.clientId ?? this._state.authorizationRequestOpts?.clientId } = opts ?? {}; let { redirectUri } = opts ?? {}; if (opts?.authorizationResponse) { diff --git a/packages/oid4vci-common/lib/functions/CredentialRequestUtil.ts b/packages/oid4vci-common/lib/functions/CredentialRequestUtil.ts index 6140125f..814a1d40 100644 --- a/packages/oid4vci-common/lib/functions/CredentialRequestUtil.ts +++ b/packages/oid4vci-common/lib/functions/CredentialRequestUtil.ts @@ -35,6 +35,8 @@ export function getTypesFromRequest(credentialRequest: CredentialRequest, opts?: } } else if (credentialRequest.format === 'vc+sd-jwt' && 'vct' in credentialRequest) { types = [credentialRequest.vct]; + } else if (credentialRequest.format === 'mso_mdoc' && 'doctype' in credentialRequest) { + types = [credentialRequest.doctype]; } if (!types || types.length === 0) { diff --git a/packages/oid4vci-common/lib/functions/FormatUtils.ts b/packages/oid4vci-common/lib/functions/FormatUtils.ts index 5d2fc9ec..2b9c87a1 100644 --- a/packages/oid4vci-common/lib/functions/FormatUtils.ts +++ b/packages/oid4vci-common/lib/functions/FormatUtils.ts @@ -17,7 +17,7 @@ export function isNotFormat { - return ['jwt_vc_json', 'jwt_vc_json-ld', 'ldp_vc', 'vc+sd-jwt'].includes(format); + return ['jwt_vc_json', 'jwt_vc_json-ld', 'ldp_vc', 'vc+sd-jwt', 'mso_mdoc'].includes(format); }; export function getUniformFormat(format: string | OID4VCICredentialFormat | CredentialFormat): OID4VCICredentialFormat { diff --git a/packages/oid4vci-common/lib/functions/ProofUtil.ts b/packages/oid4vci-common/lib/functions/ProofUtil.ts index 1a188b02..34101410 100644 --- a/packages/oid4vci-common/lib/functions/ProofUtil.ts +++ b/packages/oid4vci-common/lib/functions/ProofUtil.ts @@ -26,11 +26,12 @@ const debug = Debug('sphereon:openid4vci:common'); * If exists, verifies the ProofOfPossession * - proofOfPossessionCallbackArgs: ProofOfPossessionCallbackArgs * arguments needed for signing ProofOfPossession - * @param callbacks: * - proofOfPossessionCallback: JWTSignerCallback * Mandatory to create (sign) ProofOfPossession * - proofOfPossessionVerifierCallback?: JWTVerifyCallback * If exists, verifies the ProofOfPossession + * @param popMode + * @param callbacks * @param jwtProps * @param existingJwt * - Optional, clientId of the party requesting the credential diff --git a/packages/oid4vci-common/lib/functions/TypeConversionUtils.ts b/packages/oid4vci-common/lib/functions/TypeConversionUtils.ts index 0e8c5924..6958e07f 100644 --- a/packages/oid4vci-common/lib/functions/TypeConversionUtils.ts +++ b/packages/oid4vci-common/lib/functions/TypeConversionUtils.ts @@ -1,10 +1,18 @@ -import { AuthorizationDetails, CredentialOfferPayload, UniformCredentialOfferPayload, UniformCredentialOfferRequest, VCI_LOG_COMMON } from '../index'; +import { + AuthorizationDetails, + CredentialConfigurationSupportedMsoMdocV1_0_13, + CredentialOfferPayload, + CredentialSupportedMsoMdoc, + UniformCredentialOfferPayload, + UniformCredentialOfferRequest, + VCI_LOG_COMMON, +} from '../index'; import { CredentialConfigurationSupported, CredentialConfigurationSupportedSdJwtVcV1_0_13, CredentialDefinitionJwtVcJsonLdAndLdpVcV1_0_13, CredentialDefinitionJwtVcJsonV1_0_13, - CredentialOfferFormat, + CredentialOfferFormatV1_0_11, CredentialsSupportedLegacy, CredentialSupportedSdJwtVc, JsonLdIssuerCredentialDefinition, @@ -12,7 +20,13 @@ import { export function isW3cCredentialSupported( supported: CredentialConfigurationSupported | CredentialsSupportedLegacy, -): supported is Exclude { +): supported is Exclude< + CredentialConfigurationSupported, + | CredentialConfigurationSupportedMsoMdocV1_0_13 + | CredentialSupportedMsoMdoc + | CredentialConfigurationSupportedSdJwtVcV1_0_13 + | CredentialSupportedSdJwtVc +> { return ['jwt_vc_json', 'jwt_vc_json-ld', 'ldp_vc', 'jwt_vc'].includes(supported.format); } @@ -27,7 +41,7 @@ export const getNumberOrUndefined = (input?: string): number | undefined => { export function getTypesFromObject( subject: | CredentialConfigurationSupported - | CredentialOfferFormat + | CredentialOfferFormatV1_0_11 | CredentialDefinitionJwtVcJsonLdAndLdpVcV1_0_13 | CredentialDefinitionJwtVcJsonV1_0_13 | JsonLdIssuerCredentialDefinition @@ -49,7 +63,9 @@ export function getTypesFromObject( } else if ('type' in subject && subject.type) { return Array.isArray(subject.type) ? subject.type : [subject.type]; } else if ('vct' in subject && subject.vct) { - return [subject.vct]; + return [subject.vct as string]; + } else if ('doctype' in subject && subject.doctype) { + return [subject.doctype as string]; } VCI_LOG_COMMON.warning('Could not deduce credential types. Probably a failure down the line will happen!'); return undefined; @@ -104,6 +120,8 @@ export function getTypesFromCredentialSupported( types = getTypesFromObject(credentialSupported) ?? []; } else if (credentialSupported.format === 'vc+sd-jwt') { types = [credentialSupported.vct]; + } else if (credentialSupported.format === 'mso_mdoc') { + types = [credentialSupported.doctype]; } if (!types || types.length === 0) { diff --git a/packages/oid4vci-common/lib/types/Authorization.types.ts b/packages/oid4vci-common/lib/types/Authorization.types.ts index a2329988..f8544d54 100644 --- a/packages/oid4vci-common/lib/types/Authorization.types.ts +++ b/packages/oid4vci-common/lib/types/Authorization.types.ts @@ -74,10 +74,15 @@ export interface CommonAuthorizationRequest { * string type added for conformity with our previous code in the client */ export type AuthorizationDetails = - | (CommonAuthorizationDetails & (AuthorizationDetailsJwtVcJson | AuthorizationDetailsJwtVcJsonLdAndLdpVc | AuthorizationDetailsSdJwtVc)) + | (CommonAuthorizationDetails & + (AuthorizationDetailsJwtVcJson | AuthorizationDetailsJwtVcJsonLdAndLdpVc | AuthorizationDetailsSdJwtVc | AuthorizationDetailsMsoMdoc)) | string; -export type AuthorizationRequest = AuthorizationRequestJwtVcJson | AuthorizationRequestJwtVcJsonLdAndLdpVc | AuthorizationRequestSdJwtVc; +export type AuthorizationRequest = + | AuthorizationRequestJwtVcJson + | AuthorizationRequestJwtVcJsonLdAndLdpVc + | AuthorizationRequestSdJwtVc + | AuthorizationRequestMsoMdoc; export interface AuthorizationRequestJwtVcJson extends CommonAuthorizationRequest { authorization_details?: AuthorizationDetailsJwtVcJson[]; @@ -91,6 +96,10 @@ export interface AuthorizationRequestSdJwtVc extends CommonAuthorizationRequest authorization_details?: AuthorizationDetailsSdJwtVc[]; } +export interface AuthorizationRequestMsoMdoc extends CommonAuthorizationRequest { + authorization_details?: AuthorizationDetailsMsoMdoc[]; +} + /* export interface AuthDetails { type: 'openid_credential' | string; @@ -164,6 +173,13 @@ export interface AuthorizationDetailsSdJwtVc extends CommonAuthorizationDetails claims?: IssuerCredentialSubject; } +export interface AuthorizationDetailsMsoMdoc extends CommonAuthorizationDetails { + format: 'mso_mdoc'; + + doctype: string; + claims?: IssuerCredentialSubject; +} + export enum GrantTypes { AUTHORIZATION_CODE = 'authorization_code', PRE_AUTHORIZED_CODE = 'urn:ietf:params:oauth:grant-type:pre-authorized_code', diff --git a/packages/oid4vci-common/lib/types/CredentialIssuance.types.ts b/packages/oid4vci-common/lib/types/CredentialIssuance.types.ts index 50bc5b86..5cb140fd 100644 --- a/packages/oid4vci-common/lib/types/CredentialIssuance.types.ts +++ b/packages/oid4vci-common/lib/types/CredentialIssuance.types.ts @@ -1,18 +1,18 @@ -import { BaseJWK } from '@sphereon/oid4vc-common'; -import { W3CVerifiableCredential } from '@sphereon/ssi-types'; +import { BaseJWK } from '@sphereon/oid4vc-common' +import { IVerifiableCredential } from '@sphereon/ssi-types' -import { ExperimentalSubjectIssuance } from '../experimental/holder-vci'; +import { ExperimentalSubjectIssuance } from '../experimental/holder-vci' -import { AuthzFlowType } from './Authorization.types'; -import { OID4VCICredentialFormat, TxCode, UniformCredentialRequest } from './Generic.types'; -import { OpenId4VCIVersion } from './OpenID4VCIVersions.types'; -import { CredentialOfferPayloadV1_0_08, CredentialRequestV1_0_08 } from './v1_0_08.types'; -import { CredentialOfferPayloadV1_0_09, CredentialOfferV1_0_09 } from './v1_0_09.types'; -import { CredentialOfferPayloadV1_0_11, CredentialOfferV1_0_11, CredentialRequestV1_0_11 } from './v1_0_11.types'; -import { CredentialOfferPayloadV1_0_13, CredentialOfferV1_0_13, CredentialRequestV1_0_13 } from './v1_0_13.types'; +import { AuthzFlowType } from './Authorization.types' +import { OID4VCICredentialFormat, TxCode, UniformCredentialRequest } from './Generic.types' +import { OpenId4VCIVersion } from './OpenID4VCIVersions.types' +import { CredentialOfferPayloadV1_0_08, CredentialRequestV1_0_08 } from './v1_0_08.types' +import { CredentialOfferPayloadV1_0_09, CredentialOfferV1_0_09 } from './v1_0_09.types' +import { CredentialOfferPayloadV1_0_11, CredentialOfferV1_0_11, CredentialRequestV1_0_11 } from './v1_0_11.types' +import { CredentialOfferPayloadV1_0_13, CredentialOfferV1_0_13, CredentialRequestV1_0_13 } from './v1_0_13.types' export interface CredentialResponse extends ExperimentalSubjectIssuance { - credential?: W3CVerifiableCredential; // OPTIONAL. Contains issued Credential. MUST be present when acceptance_token is not returned. MAY be a JSON string or a JSON object, depending on the Credential format. See Appendix E for the Credential format specific encoding requirements + credential?: IVerifiableCredential | string; // OPTIONAL. Contains issued Credential. MUST be present when acceptance_token is not returned. MAY be a JSON string or a JSON object, depending on the Credential format. See Appendix E for the Credential format specific encoding requirements format?: OID4VCICredentialFormat /* | OID4VCICredentialFormat[]*/; // REQUIRED. JSON string denoting the format of the issued Credential TODO: remove when cleaning