Skip to content

Commit

Permalink
feat: Add support for mDL / mdoc to the OID4VCI client
Browse files Browse the repository at this point in the history
  • Loading branch information
nklomp committed Aug 12, 2024
1 parent 5e06969 commit 6556cc0
Show file tree
Hide file tree
Showing 19 changed files with 137 additions and 39 deletions.
4 changes: 3 additions & 1 deletion packages/client/lib/AuthorizationCodeClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CredentialDefinitionJwtVcJsonV1_0_13 | CredentialDefinitionJwtVcJsonLdAndLdpVcV1_0_13> =
undefined;
if (isW3cCredentialSupported(cred)) {
Expand All @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions packages/client/lib/AuthorizationCodeClientV1_0_11.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
CodeChallengeMethod,
convertJsonToURI,
CreateRequestObjectMode,
CredentialOfferFormat,
CredentialOfferFormatV1_0_11,
CredentialOfferPayloadV1_0_11,
CredentialOfferRequestWithBaseUrl,
CredentialsSupportedLegacy,
Expand Down Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion packages/client/lib/CredentialRequestClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down
10 changes: 10 additions & 0 deletions packages/client/lib/CredentialRequestClientV1_0_11.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down
3 changes: 2 additions & 1 deletion packages/client/lib/MetadataClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion packages/client/lib/MetadataClientV1_0_13.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/client/lib/OpenID4VCIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ export class OpenID4VCIClient {
authorizationResponse?: string | AuthorizationResponse; // Pass in an auth response, either as URI/redirect, or object
additionalRequestParams?: Record<string, any>;
},
): Promise<AccessTokenResponse & {params?: DPoPResponseParams}> {
): Promise<AccessTokenResponse & { params?: DPoPResponseParams }> {
const { pin, clientId = this._state.clientId ?? this._state.authorizationRequestOpts?.clientId } = opts ?? {};
let { redirectUri } = opts ?? {};
if (opts?.authorizationResponse) {
Expand Down
2 changes: 1 addition & 1 deletion packages/client/lib/OpenID4VCIClientV1_0_11.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any>;
},
): Promise<AccessTokenResponse & {params?: DPoPResponseParams}> {
): Promise<AccessTokenResponse & { params?: DPoPResponseParams }> {
const { pin, clientId = this._state.clientId ?? this._state.authorizationRequestOpts?.clientId } = opts ?? {};
let { redirectUri } = opts ?? {};
if (opts?.authorizationResponse) {
Expand Down
2 changes: 1 addition & 1 deletion packages/client/lib/OpenID4VCIClientV1_0_13.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any>;
},
): Promise<AccessTokenResponse & {params?: DPoPResponseParams}> {
): Promise<AccessTokenResponse & { params?: DPoPResponseParams }> {
const { pin, clientId = this._state.clientId ?? this._state.authorizationRequestOpts?.clientId } = opts ?? {};
let { redirectUri } = opts ?? {};
if (opts?.authorizationResponse) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion packages/oid4vci-common/lib/functions/FormatUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function isNotFormat<T extends { format?: OID4VCICredentialFormat }, Form
}

const isUniformFormat = (format: string): format is OID4VCICredentialFormat => {
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 {
Expand Down
3 changes: 2 additions & 1 deletion packages/oid4vci-common/lib/functions/ProofUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 23 additions & 5 deletions packages/oid4vci-common/lib/functions/TypeConversionUtils.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,32 @@
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,
} from '../types';

export function isW3cCredentialSupported(
supported: CredentialConfigurationSupported | CredentialsSupportedLegacy,
): supported is Exclude<CredentialConfigurationSupported, CredentialConfigurationSupportedSdJwtVcV1_0_13 | CredentialSupportedSdJwtVc> {
): 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);
}

Expand All @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
20 changes: 18 additions & 2 deletions packages/oid4vci-common/lib/types/Authorization.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand All @@ -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;
Expand Down Expand Up @@ -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',
Expand Down
22 changes: 11 additions & 11 deletions packages/oid4vci-common/lib/types/CredentialIssuance.types.ts
Original file line number Diff line number Diff line change
@@ -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 <v13
transaction_id?: string; //OPTIONAL. A string identifying a Deferred Issuance transaction. This claim is contained in the response if the Credential Issuer was unable to immediately issue the credential. The value is subsequently used to obtain the respective Credential with the Deferred Credential Endpoint (see Section 9). It MUST be present when the credential parameter is not returned. It MUST be invalidated after the credential for which it was meant has been obtained by the Wallet.
acceptance_token?: string; //deprecated // OPTIONAL. A JSON string containing a security token subsequently used to obtain a Credential. MUST be present when credential is not returned
Expand Down
38 changes: 32 additions & 6 deletions packages/oid4vci-common/lib/types/Generic.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
export type InputCharSet = 'numeric' | 'text';
export type KeyProofType = 'jwt' | 'cwt' | 'ldp_vp';

export type PoPMode = 'pop' | 'JWT'; // Proof of posession, or regular JWT
export type PoPMode = 'pop' | 'JWT'; // Proof of possession, or regular JWT

/**
* Important Note: please be aware that these Common interfaces are based on versions v1_0.11 and v1_0.09
Expand All @@ -29,7 +29,7 @@ export interface ImageInfo {
[key: string]: unknown;
}

export type OID4VCICredentialFormat = 'jwt_vc_json' | 'jwt_vc_json-ld' | 'ldp_vc' | 'vc+sd-jwt' | 'jwt_vc'; // jwt_vc is added for backwards compat /*| 'mso_mdoc'*/; // we do not support mdocs at this point
export type OID4VCICredentialFormat = 'jwt_vc_json' | 'jwt_vc_json-ld' | 'ldp_vc' | 'vc+sd-jwt' | 'jwt_vc' | 'mso_mdoc'; // jwt_vc is added for backwards compat

export interface NameAndLocale {
name?: string; // REQUIRED. String value of a display name for the Credential.
Expand Down Expand Up @@ -164,12 +164,22 @@ export interface CredentialSupportedSdJwtVc extends CommonCredentialSupported {
order?: string[]; //An array of claims.display.name values that lists them in the order they should be displayed by the Wallet.
}

export interface CredentialSupportedMsoMdoc extends CommonCredentialSupported {
format: 'mso_mdoc';

doctype: string;
claims?: IssuerCredentialSubject;

order?: string[]; //An array of claims.display.name values that lists them in the order they should be displayed by the Wallet.
}

export type CredentialConfigurationSupported =
| CredentialConfigurationSupportedV1_0_13
| (CommonCredentialSupported & (CredentialSupportedJwtVcJson | CredentialSupportedJwtVcJsonLdAndLdpVc | CredentialSupportedSdJwtVc));
| (CommonCredentialSupported &
(CredentialSupportedJwtVcJson | CredentialSupportedJwtVcJsonLdAndLdpVc | CredentialSupportedSdJwtVc | CredentialSupportedMsoMdoc));

export type CredentialsSupportedLegacy = CommonCredentialSupported &
(CredentialSupportedJwtVcJson | CredentialSupportedJwtVcJsonLdAndLdpVc | CredentialSupportedSdJwtVc);
(CredentialSupportedJwtVcJson | CredentialSupportedJwtVcJsonLdAndLdpVc | CredentialSupportedSdJwtVc | CredentialSupportedMsoMdoc);

export interface CommonCredentialOfferFormat {
format: OID4VCICredentialFormat | string;
Expand All @@ -196,8 +206,18 @@ export interface CredentialOfferFormatSdJwtVc extends CommonCredentialOfferForma
claims?: IssuerCredentialSubject;
}

export type CredentialOfferFormat = CommonCredentialOfferFormat &
(CredentialOfferFormatJwtVcJsonLdAndLdpVc | CredentialOfferFormatJwtVcJson | CredentialOfferFormatSdJwtVc);
// NOTE: the sd-jwt format is added to oid4vci in a later draft version than currently
// supported, so there's no defined offer format. However, based on the request structure
// we support sd-jwt for older drafts of oid4vci as well
export interface CredentialOfferFormatMsoMdoc extends CommonCredentialOfferFormat {
format: 'mso_mdoc';

doctype: string;
claims?: IssuerCredentialSubject;
}

export type CredentialOfferFormatV1_0_11 = CommonCredentialOfferFormat &
(CredentialOfferFormatJwtVcJsonLdAndLdpVc | CredentialOfferFormatJwtVcJson | CredentialOfferFormatSdJwtVc | CredentialOfferFormatMsoMdoc);

/**
* Optional storage that can help the credential Data Supplier. For instance to store credential input data during offer creation, if no additional data can be supplied later on
Expand Down Expand Up @@ -249,6 +269,12 @@ export interface CredentialRequestSdJwtVc extends CommonCredentialRequest {
claims?: IssuerCredentialSubject;
}

export interface CredentialRequestMsoMdoc extends CommonCredentialRequest {
format: 'mso_mdoc';
doctype: string;
claims?: IssuerCredentialSubject;
}

export interface CommonCredentialResponse extends ExperimentalSubjectIssuance {
// format: string; TODO do we still need this for previous version support?
credential?: W3CVerifiableCredential;
Expand Down
Loading

0 comments on commit 6556cc0

Please sign in to comment.