Skip to content

Commit

Permalink
fix: oid4vci draft 13 typing
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <[email protected]>
  • Loading branch information
TimoGlastra committed Jul 19, 2024
1 parent e264b44 commit 6d0bfc9
Show file tree
Hide file tree
Showing 16 changed files with 195 additions and 129 deletions.
59 changes: 32 additions & 27 deletions packages/client/lib/AuthorizationCodeClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ import {
convertJsonToURI,
CreateRequestObjectMode,
CredentialConfigurationSupportedV1_0_13,
CredentialDefinitionJwtVcJsonLdAndLdpVcV1_0_13,
CredentialDefinitionJwtVcJsonV1_0_13,
CredentialOfferPayloadV1_0_13,
CredentialOfferRequestWithBaseUrl,
determineSpecVersionFromOffer,
EndpointMetadataResultV1_0_13,
formPost,
isW3cCredentialSupported,
JsonURIMode,
Jwt,
OID4VCICredentialFormat,
OpenId4VCIVersion,
PARMode,
PKCEOpts,
Expand Down Expand Up @@ -95,14 +97,17 @@ export const createAuthorizationRequestUrl = async ({
clientId?: string;
version?: OpenId4VCIVersion;
}): Promise<string> => {
function removeDisplayAndValueTypes(obj: any): void {
for (const prop in obj) {
function removeDisplayAndValueTypes(obj: any) {
const newObj = { ...obj };
for (const prop in newObj) {
if (['display', 'value_type'].includes(prop)) {
delete obj[prop];
} else if (typeof obj[prop] === 'object') {
removeDisplayAndValueTypes(obj[prop]);
delete newObj[prop];
} else if (typeof newObj[prop] === 'object') {
newObj[prop] = removeDisplayAndValueTypes(newObj[prop]);
}
}

return newObj;
}

const { redirectUri, requestObjectOpts = { requestObjectMode: CreateRequestObjectMode.NONE } } = authorizationRequest;
Expand All @@ -111,7 +116,7 @@ export const createAuthorizationRequestUrl = async ({
let { scope, authorizationDetails } = authorizationRequest;
const parMode = endpointMetadata?.credentialIssuerMetadata?.require_pushed_authorization_requests
? PARMode.REQUIRE
: (authorizationRequest.parMode ?? (client_id ? PARMode.AUTO : PARMode.NEVER));
: authorizationRequest.parMode ?? (client_id ? PARMode.AUTO : PARMode.NEVER);
// Scope and authorization_details can be used in the same authorization request
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param
if (!scope && !authorizationDetails) {
Expand All @@ -127,42 +132,42 @@ export const createAuthorizationRequestUrl = async ({
? filterSupportedCredentials(credentialOffer.credential_offer as CredentialOfferPayloadV1_0_13, credentialConfigurationSupported)
: [];

// FIXME: complains about VCT for sd-jwt
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
authorizationDetails = creds.flatMap((cred) => {
const locations = [credentialOffer?.credential_offer.credential_issuer ?? endpointMetadata.issuer];

// TODO: credential_configuration_id seems to always be defined?
const credential_configuration_id: string | undefined = cred.configuration_id;
const vct: string | undefined = cred.vct;
let format: OID4VCICredentialFormat | undefined;
const format = credential_configuration_id ? undefined : cred.format;

if (!credential_configuration_id) {
format = cred.format;
}
if (!credential_configuration_id && !cred.format) {
throw Error('format is required in authorization details');
}

const meta: any = {};
const credential_definition = cred.credential_definition;
if (credential_definition?.type && !format) {
// ype: OPTIONAL. Array as defined in Appendix A.1.1.2. This claim contains the type values the Wallet requests authorization for at the Credential Issuer. It MUST be present if the claim format is present in the root of the authorization details object. It MUST not be present otherwise.
// It meens we have a config_id, already mapping it to an explicit format and types
delete credential_definition.type;
}
if (credential_definition.credentialSubject) {
removeDisplayAndValueTypes(credential_definition.credentialSubject);
// SD-JWT VC
const vct = cred.format === 'vc+sd-jwt' ? cred.vct : undefined;

// W3C credentials
let credential_definition: undefined | Partial<CredentialDefinitionJwtVcJsonV1_0_13 | CredentialDefinitionJwtVcJsonLdAndLdpVcV1_0_13> =
undefined;
if (isW3cCredentialSupported(cred)) {
credential_definition = {
...cred.credential_definition,
// type: OPTIONAL. Array as defined in Appendix A.1.1.2. This claim contains the type values the Wallet requests authorization for at the Credential Issuer. It MUST be present if the claim format is present in the root of the authorization details object. It MUST not be present otherwise.
// It meens we have a config_id, already mapping it to an explicit format and types
type: format ? cred.credential_definition.type : undefined,
credentialSubject: cred.credential_definition.credentialSubject
? removeDisplayAndValueTypes(cred.credential_definition.credentialSubject)
: undefined,
};
}

return {
type: 'openid_credential',
...meta,
locations,
...(credential_definition && { credential_definition }),
...(credential_configuration_id && { credential_configuration_id }),
...(format && { format }),
...(vct && { vct }),
...(cred.claims && { claims: removeDisplayAndValueTypes(JSON.parse(JSON.stringify(cred.claims))) }),
...(vct && { vct, claims: cred.claims ? removeDisplayAndValueTypes(cred.claims) : undefined }),
} as AuthorizationDetails;
});
if (!authorizationDetails || authorizationDetails.length === 0) {
Expand Down
2 changes: 1 addition & 1 deletion packages/client/lib/AuthorizationCodeClientV1_0_11.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const createAuthorizationRequestUrlV1_0_11 = async ({

const parMode = endpointMetadata?.credentialIssuerMetadata?.require_pushed_authorization_requests
? PARMode.REQUIRE
: (authorizationRequest.parMode ?? PARMode.AUTO);
: authorizationRequest.parMode ?? PARMode.AUTO;
// Scope and authorization_details can be used in the same authorization request
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param
if (!scope && !authorizationDetails) {
Expand Down
16 changes: 7 additions & 9 deletions packages/client/lib/CredentialRequestClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
getUniformFormat,
isDeferredCredentialResponse,
isValidURL,
JsonLdIssuerCredentialDefinition,
OID4VCICredentialFormat,
OpenId4VCIVersion,
OpenIDResponse,
Expand Down Expand Up @@ -203,7 +202,9 @@ export class CredentialRequestClient {
// TODO: we should move format specific logic
if (format === 'jwt_vc_json' || format === 'jwt_vc') {
return {
types,
credential_definition: {
type: types,
},
format,
proof,
...opts.subjectIssuance,
Expand All @@ -218,13 +219,10 @@ export class CredentialRequestClient {
proof,
...opts.subjectIssuance,

// Ignored because v11 does not have the context value, but it is required in v12
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
credential_definition: {
types,
...(opts.context && { '@context': opts.context }),
} as JsonLdIssuerCredentialDefinition,
type: types,
'@context': opts.context as string[],
},
};
} else if (format === 'vc+sd-jwt') {
if (types.length > 1) {
Expand All @@ -236,7 +234,7 @@ export class CredentialRequestClient {
proof,
vct: types[0],
...opts.subjectIssuance,
} as CredentialRequestV1_0_13;
};
}

throw new Error(`Unsupported format: ${format}`);
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 @@ -549,7 +549,7 @@ export class OpenID4VCIClient {
issuerSupportedFlowTypes(): AuthzFlowType[] {
return (
this.credentialOffer?.supportedFlows ??
((this._state.endpointMetadata?.credentialIssuerMetadata?.authorization_endpoint ?? this._state.endpointMetadata?.authorization_server)
(this._state.endpointMetadata?.credentialIssuerMetadata?.authorization_endpoint ?? this._state.endpointMetadata?.authorization_server
? [AuthzFlowType.AUTHORIZATION_CODE_FLOW]
: [])
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import { KeyObject } from 'crypto';

import {
Alg,
CredentialIssuerMetadataV1_0_13,
CredentialRequestV1_0_13,
Jwt,
JwtVerifyResult,
OpenId4VCIVersion,
ProofOfPossession,
} from '@sphereon/oid4vci-common';
import { Alg, CredentialIssuerMetadataV1_0_13, Jwt, JwtVerifyResult, OpenId4VCIVersion, ProofOfPossession } from '@sphereon/oid4vci-common';
import * as jose from 'jose';

import { CredentialRequestOpts, ProofOfPossessionBuilder } from '..';
Expand Down Expand Up @@ -112,7 +104,7 @@ describe('Credential Request Client Builder', () => {
.withKid(kid)
.build();
await proofOfPossessionVerifierCallbackFunction({ ...proof, kid });
const credentialRequest: CredentialRequestV1_0_13 = await credReqClient.createCredentialRequest({
const credentialRequest = await credReqClient.createCredentialRequest({
proofInput: proof,
credentialIdentifier: 'OpenBadgeCredential',
version: OpenId4VCIVersion.VER_1_0_13,
Expand Down Expand Up @@ -142,7 +134,7 @@ describe('Credential Request Client Builder', () => {
.withKid(kid_withoutDid)
.build();
await proofOfPossessionVerifierCallbackFunction({ ...proof, kid: kid_withoutDid });
const credentialRequest: CredentialRequestV1_0_13 = await credReqClient.createCredentialRequest({
const credentialRequest = await credReqClient.createCredentialRequest({
proofInput: proof,
credentialTypes: 'OpenBadgeCredential',
version: OpenId4VCIVersion.VER_1_0_13,
Expand Down
8 changes: 4 additions & 4 deletions packages/client/lib/__tests__/SdJwt.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
AccessTokenRequest,
CredentialConfigurationSupportedSdJwtVcV1_0_13,
CredentialConfigurationSupportedV1_0_13,
CredentialRequestV1_0_13,
CredentialSupportedSdJwtVc,
} from '@sphereon/oid4vci-common';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
Expand Down Expand Up @@ -109,7 +109,7 @@ describe('sd-jwt vc', () => {
const supported = client.getCredentialsSupported('vc+sd-jwt');
expect(supported).toEqual({ SdJwtCredentialId: { format: 'vc+sd-jwt', id: 'SdJwtCredentialId', vct: 'SdJwtCredentialId' } });

const offered = supported['SdJwtCredentialId'] as CredentialSupportedSdJwtVc;
const offered = supported['SdJwtCredentialId'] as CredentialConfigurationSupportedSdJwtVcV1_0_13;

nock(issuerMetadata.token_endpoint as string)
.post('/')
Expand All @@ -130,7 +130,7 @@ describe('sd-jwt vc', () => {
.post('/')
.reply(200, async (_, body) =>
vcIssuer.issueCredential({
credentialRequest: { ...(body as CredentialRequestV1_0_13), credential_identifier: offered.vct },
credentialRequest: { ...(body as any), credential_identifier: 'SdJwtCredentialId' },
credential: {
vct: 'Hello',
iss: 'did:example:123',
Expand Down Expand Up @@ -233,7 +233,7 @@ describe('sd-jwt vc', () => {
.post('/')
.reply(200, async (_, body) =>
vcIssuer.issueCredential({
credentialRequest: { ...(body as CredentialRequestV1_0_13), credential_identifier: offered.vct },
credentialRequest: { ...(body as any), credential_identifier: offered.vct },
credential: {
vct: 'Hello',
iss: 'example.com',
Expand Down
36 changes: 23 additions & 13 deletions packages/common/lib/functions/CredentialRequestUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,31 @@ import { getFormatForVersion } from './FormatUtils';

export function getTypesFromRequest(credentialRequest: CredentialRequest, opts?: { filterVerifiableCredential: boolean }) {
let types: string[] = [];
console.log(credentialRequest);
if ('credential_identifier' in credentialRequest && credentialRequest.credential_identifier) {
throw Error(`Cannot get types from request when it contains a credential_identifier`);
} else if (credentialRequest.format === 'jwt_vc_json' || credentialRequest.format === 'jwt_vc') {
types = 'types' in credentialRequest ? credentialRequest.types : [];
} else if (credentialRequest.format === 'jwt_vc_json-ld' || credentialRequest.format === 'ldp_vc') {
types =
'credential_definition' in credentialRequest && credentialRequest.credential_definition
? credentialRequest.credential_definition.types
: // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
'types' in credentialRequest.types
? (credentialRequest['types' as keyof CredentialRequest] as unknown as string[])
: [];
} else if (credentialRequest.format === 'vc+sd-jwt') {
types = 'vct' in credentialRequest ? [credentialRequest.vct as string] : [];
} else if (
credentialRequest.format === 'jwt_vc_json-ld' ||
credentialRequest.format === 'ldp_vc' ||
credentialRequest.format === 'jwt_vc' ||
credentialRequest.format === 'jwt_vc_json'
) {
if ('credential_definition' in credentialRequest && credentialRequest.credential_definition) {
types =
'types' in credentialRequest.credential_definition
? credentialRequest.credential_definition.types
: credentialRequest.credential_definition.type;
}

if ('type' in credentialRequest && Array.isArray(credentialRequest.type)) {
types = credentialRequest.type;
}

if ('types' in credentialRequest && Array.isArray(credentialRequest.types)) {
types = credentialRequest.types;
}
} else if (credentialRequest.format === 'vc+sd-jwt' && 'vct' in credentialRequest) {
types = [credentialRequest.vct];
}

if (!types || types.length === 0) {
Expand Down
16 changes: 7 additions & 9 deletions packages/common/lib/functions/IssuerMetadataUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getTypesFromObject, VCI_LOG_COMMON } from '../index';
import { getTypesFromObject, isW3cCredentialSupported, VCI_LOG_COMMON } from '../index';
import {
AuthorizationServerMetadata,
CredentialConfigurationSupported,
Expand Down Expand Up @@ -112,13 +112,11 @@ export function getSupportedCredential(opts?: {
} else if (types) {
isTypeMatch = normalizedTypes.every((type) => types.includes(type));
} else {
if ('credential_definition' in config) {
isTypeMatch = normalizedTypes.every((type) => config.credential_definition.type?.includes(type));
} else if ('type' in config && Array.isArray(config.type)) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
isTypeMatch = normalizedTypes.every((type) => config.type.includes(type));
} else if ('types' in config) {
if (isW3cCredentialSupported(config) && 'credential_definition' in config) {
isTypeMatch = normalizedTypes.every((type) => config.credential_definition.type.includes(type));
} else if (isW3cCredentialSupported(config) && 'type' in config && Array.isArray(config.type)) {
isTypeMatch = normalizedTypes.every((type) => (config.type as string[]).includes(type));
} else if (isW3cCredentialSupported(config) && 'types' in config) {
isTypeMatch = normalizedTypes.every((type) => config.types?.includes(type));
}
}
Expand Down Expand Up @@ -183,7 +181,7 @@ export function getIssuerDisplays(metadata: CredentialIssuerMetadata | IssuerMet
metadata.display?.filter(
(item) => !opts?.prefLocales || opts.prefLocales.length === 0 || (item.locale && opts.prefLocales.includes(item.locale)) || !item.locale,
) ?? [];
return matchedDisplays.sort((item) => (item.locale ? (opts?.prefLocales.indexOf(item.locale) ?? 1) : Number.MAX_VALUE));
return matchedDisplays.sort((item) => (item.locale ? opts?.prefLocales.indexOf(item.locale) ?? 1 : Number.MAX_VALUE));
}

/**
Expand Down
29 changes: 24 additions & 5 deletions packages/common/lib/functions/TypeConversionUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
import { AuthorizationDetails, CredentialOfferPayload, UniformCredentialOfferPayload, UniformCredentialOfferRequest, VCI_LOG_COMMON } from '../index';
import { CredentialConfigurationSupported, CredentialDefinitionV1_0_13, CredentialOfferFormat, JsonLdIssuerCredentialDefinition } from '../types';
import {
CredentialConfigurationSupported,
CredentialConfigurationSupportedSdJwtVcV1_0_13,
CredentialDefinitionJwtVcJsonLdAndLdpVcV1_0_13,
CredentialDefinitionJwtVcJsonV1_0_13,
CredentialOfferFormat,
CredentialsSupportedLegacy,
CredentialSupportedSdJwtVc,
JsonLdIssuerCredentialDefinition,
} from '../types';

export function isW3cCredentialSupported(
supported: CredentialConfigurationSupported | CredentialsSupportedLegacy,
): supported is Exclude<CredentialConfigurationSupported, CredentialConfigurationSupportedSdJwtVcV1_0_13 | CredentialSupportedSdJwtVc> {
return ['jwt_vc_json', 'jwt_vc_json-ld', 'ldp_vc', 'jwt_vc'].includes(supported.format);
}

export const getNumberOrUndefined = (input?: string): number | undefined => {
return input && !isNaN(+input) ? +input : undefined;
Expand All @@ -10,13 +25,19 @@ export const getNumberOrUndefined = (input?: string): number | undefined => {
* @param subject
*/
export function getTypesFromObject(
subject: CredentialConfigurationSupported | CredentialOfferFormat | CredentialDefinitionV1_0_13 | JsonLdIssuerCredentialDefinition | string,
subject:
| CredentialConfigurationSupported
| CredentialOfferFormat
| CredentialDefinitionJwtVcJsonLdAndLdpVcV1_0_13
| CredentialDefinitionJwtVcJsonV1_0_13
| JsonLdIssuerCredentialDefinition
| string,
): string[] | undefined {
if (subject === undefined) {
return undefined;
} else if (typeof subject === 'string') {
return [subject];
} else if ('credential_definition' in subject && subject.credential_definition) {
} else if ('format' in subject && subject.format !== 'vc+sd-jwt' && 'credential_definition' in subject) {
return getTypesFromObject(subject.credential_definition);
} else if ('types' in subject && subject.types) {
return Array.isArray(subject.types) ? subject.types : [subject.types];
Expand Down Expand Up @@ -77,8 +98,6 @@ export function getTypesFromCredentialSupported(
) {
types = getTypesFromObject(credentialSupported) ?? [];
} else if (credentialSupported.format === 'vc+sd-jwt') {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
types = [credentialSupported.vct];
}

Expand Down
6 changes: 5 additions & 1 deletion packages/common/lib/types/Generic.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,17 @@ export interface ProofType {
proof_signing_alg_values_supported: string[];
}

export type ProofTypesSupported = {
[key in KeyProofType]?: ProofType;
};

export type CommonCredentialSupported = CredentialSupportedBrief &
ExperimentalSubjectIssuance & {
format: OID4VCICredentialFormat | string; //REQUIRED. A JSON string identifying the format of this credential, e.g. jwt_vc_json or ldp_vc.
id?: string; // OPTIONAL. A JSON string identifying the respective object. The value MUST be unique across all credentials_supported entries in the Credential Issuer Metadata
display?: CredentialsSupportedDisplay[]; // OPTIONAL. An array of objects, where each object contains the display properties of the supported credential for a certain language
scope?: string; // OPTIONAL. A JSON string identifying the scope value that this Credential Issuer supports for this particular Credential. The value can be the same across multiple credential_configurations_supported objects. The Authorization Server MUST be able to uniquely identify the Credential Issuer based on the scope value. The Wallet can use this value in the Authorization Request as defined in Section 5.1.2. Scope values in this Credential Issuer metadata MAY duplicate those in the scopes_supported parameter of the Authorization Server.
proof_types_supported?: Record<KeyProofType, ProofType>;
proof_types_supported?: ProofTypesSupported;

/**
* following properties are non-mso_mdoc specific and we might wanna rethink them when we're going to support mso_mdoc
Expand Down
Loading

0 comments on commit 6d0bfc9

Please sign in to comment.