Skip to content

Commit c5cb3cd

Browse files
authored
Merge pull request #129 from TimoGlastra/fix/draft-13-metadata
fix: oid4vci draft 13 typing
2 parents e264b44 + fbc222e commit c5cb3cd

16 files changed

Lines changed: 195 additions & 130 deletions

packages/client/lib/AuthorizationCodeClient.ts

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ import {
55
convertJsonToURI,
66
CreateRequestObjectMode,
77
CredentialConfigurationSupportedV1_0_13,
8+
CredentialDefinitionJwtVcJsonLdAndLdpVcV1_0_13,
9+
CredentialDefinitionJwtVcJsonV1_0_13,
810
CredentialOfferPayloadV1_0_13,
911
CredentialOfferRequestWithBaseUrl,
1012
determineSpecVersionFromOffer,
1113
EndpointMetadataResultV1_0_13,
1214
formPost,
15+
isW3cCredentialSupported,
1316
JsonURIMode,
1417
Jwt,
15-
OID4VCICredentialFormat,
1618
OpenId4VCIVersion,
1719
PARMode,
1820
PKCEOpts,
@@ -95,14 +97,17 @@ export const createAuthorizationRequestUrl = async ({
9597
clientId?: string;
9698
version?: OpenId4VCIVersion;
9799
}): Promise<string> => {
98-
function removeDisplayAndValueTypes(obj: any): void {
99-
for (const prop in obj) {
100+
function removeDisplayAndValueTypes(obj: any) {
101+
const newObj = { ...obj };
102+
for (const prop in newObj) {
100103
if (['display', 'value_type'].includes(prop)) {
101-
delete obj[prop];
102-
} else if (typeof obj[prop] === 'object') {
103-
removeDisplayAndValueTypes(obj[prop]);
104+
delete newObj[prop];
105+
} else if (typeof newObj[prop] === 'object') {
106+
newObj[prop] = removeDisplayAndValueTypes(newObj[prop]);
104107
}
105108
}
109+
110+
return newObj;
106111
}
107112

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

130-
// FIXME: complains about VCT for sd-jwt
131-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
132-
// @ts-ignore
133135
authorizationDetails = creds.flatMap((cred) => {
134136
const locations = [credentialOffer?.credential_offer.credential_issuer ?? endpointMetadata.issuer];
137+
138+
// TODO: credential_configuration_id seems to always be defined?
135139
const credential_configuration_id: string | undefined = cred.configuration_id;
136-
const vct: string | undefined = cred.vct;
137-
let format: OID4VCICredentialFormat | undefined;
140+
const format = credential_configuration_id ? undefined : cred.format;
138141

139-
if (!credential_configuration_id) {
140-
format = cred.format;
141-
}
142142
if (!credential_configuration_id && !cred.format) {
143143
throw Error('format is required in authorization details');
144144
}
145145

146-
const meta: any = {};
147-
const credential_definition = cred.credential_definition;
148-
if (credential_definition?.type && !format) {
149-
// 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.
150-
// It meens we have a config_id, already mapping it to an explicit format and types
151-
delete credential_definition.type;
152-
}
153-
if (credential_definition.credentialSubject) {
154-
removeDisplayAndValueTypes(credential_definition.credentialSubject);
146+
// SD-JWT VC
147+
const vct = cred.format === 'vc+sd-jwt' ? cred.vct : undefined;
148+
149+
// W3C credentials
150+
let credential_definition: undefined | Partial<CredentialDefinitionJwtVcJsonV1_0_13 | CredentialDefinitionJwtVcJsonLdAndLdpVcV1_0_13> =
151+
undefined;
152+
if (isW3cCredentialSupported(cred)) {
153+
credential_definition = {
154+
...cred.credential_definition,
155+
// 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.
156+
// It meens we have a config_id, already mapping it to an explicit format and types
157+
type: format ? cred.credential_definition.type : undefined,
158+
credentialSubject: cred.credential_definition.credentialSubject
159+
? removeDisplayAndValueTypes(cred.credential_definition.credentialSubject)
160+
: undefined,
161+
};
155162
}
156163

157164
return {
158165
type: 'openid_credential',
159-
...meta,
160166
locations,
161167
...(credential_definition && { credential_definition }),
162168
...(credential_configuration_id && { credential_configuration_id }),
163169
...(format && { format }),
164-
...(vct && { vct }),
165-
...(cred.claims && { claims: removeDisplayAndValueTypes(JSON.parse(JSON.stringify(cred.claims))) }),
170+
...(vct && { vct, claims: cred.claims ? removeDisplayAndValueTypes(cred.claims) : undefined }),
166171
} as AuthorizationDetails;
167172
});
168173
if (!authorizationDetails || authorizationDetails.length === 0) {

packages/client/lib/AuthorizationCodeClientV1_0_11.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export const createAuthorizationRequestUrlV1_0_11 = async ({
4040

4141
const parMode = endpointMetadata?.credentialIssuerMetadata?.require_pushed_authorization_requests
4242
? PARMode.REQUIRE
43-
: (authorizationRequest.parMode ?? PARMode.AUTO);
43+
: authorizationRequest.parMode ?? PARMode.AUTO;
4444
// Scope and authorization_details can be used in the same authorization request
4545
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param
4646
if (!scope && !authorizationDetails) {

packages/client/lib/CredentialRequestClient.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
getUniformFormat,
77
isDeferredCredentialResponse,
88
isValidURL,
9-
JsonLdIssuerCredentialDefinition,
109
OID4VCICredentialFormat,
1110
OpenId4VCIVersion,
1211
OpenIDResponse,
@@ -203,7 +202,9 @@ export class CredentialRequestClient {
203202
// TODO: we should move format specific logic
204203
if (format === 'jwt_vc_json' || format === 'jwt_vc') {
205204
return {
206-
types,
205+
credential_definition: {
206+
type: types,
207+
},
207208
format,
208209
proof,
209210
...opts.subjectIssuance,
@@ -218,13 +219,10 @@ export class CredentialRequestClient {
218219
proof,
219220
...opts.subjectIssuance,
220221

221-
// Ignored because v11 does not have the context value, but it is required in v12
222-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
223-
// @ts-ignore
224222
credential_definition: {
225-
types,
226-
...(opts.context && { '@context': opts.context }),
227-
} as JsonLdIssuerCredentialDefinition,
223+
type: types,
224+
'@context': opts.context as string[],
225+
},
228226
};
229227
} else if (format === 'vc+sd-jwt') {
230228
if (types.length > 1) {
@@ -236,7 +234,7 @@ export class CredentialRequestClient {
236234
proof,
237235
vct: types[0],
238236
...opts.subjectIssuance,
239-
} as CredentialRequestV1_0_13;
237+
};
240238
}
241239

242240
throw new Error(`Unsupported format: ${format}`);

packages/client/lib/OpenID4VCIClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,7 @@ export class OpenID4VCIClient {
549549
issuerSupportedFlowTypes(): AuthzFlowType[] {
550550
return (
551551
this.credentialOffer?.supportedFlows ??
552-
((this._state.endpointMetadata?.credentialIssuerMetadata?.authorization_endpoint ?? this._state.endpointMetadata?.authorization_server)
552+
(this._state.endpointMetadata?.credentialIssuerMetadata?.authorization_endpoint ?? this._state.endpointMetadata?.authorization_server
553553
? [AuthzFlowType.AUTHORIZATION_CODE_FLOW]
554554
: [])
555555
);

packages/client/lib/__tests__/CredentialRequestClientBuilder.spec.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
11
import { KeyObject } from 'crypto';
22

3-
import {
4-
Alg,
5-
CredentialIssuerMetadataV1_0_13,
6-
CredentialRequestV1_0_13,
7-
Jwt,
8-
JwtVerifyResult,
9-
OpenId4VCIVersion,
10-
ProofOfPossession,
11-
} from '@sphereon/oid4vci-common';
3+
import { Alg, CredentialIssuerMetadataV1_0_13, Jwt, JwtVerifyResult, OpenId4VCIVersion, ProofOfPossession } from '@sphereon/oid4vci-common';
124
import * as jose from 'jose';
135

146
import { CredentialRequestOpts, ProofOfPossessionBuilder } from '..';
@@ -112,7 +104,7 @@ describe('Credential Request Client Builder', () => {
112104
.withKid(kid)
113105
.build();
114106
await proofOfPossessionVerifierCallbackFunction({ ...proof, kid });
115-
const credentialRequest: CredentialRequestV1_0_13 = await credReqClient.createCredentialRequest({
107+
const credentialRequest = await credReqClient.createCredentialRequest({
116108
proofInput: proof,
117109
credentialIdentifier: 'OpenBadgeCredential',
118110
version: OpenId4VCIVersion.VER_1_0_13,
@@ -142,7 +134,7 @@ describe('Credential Request Client Builder', () => {
142134
.withKid(kid_withoutDid)
143135
.build();
144136
await proofOfPossessionVerifierCallbackFunction({ ...proof, kid: kid_withoutDid });
145-
const credentialRequest: CredentialRequestV1_0_13 = await credReqClient.createCredentialRequest({
137+
const credentialRequest = await credReqClient.createCredentialRequest({
146138
proofInput: proof,
147139
credentialTypes: 'OpenBadgeCredential',
148140
version: OpenId4VCIVersion.VER_1_0_13,

packages/client/lib/__tests__/SdJwt.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
22
AccessTokenRequest,
3+
CredentialConfigurationSupportedSdJwtVcV1_0_13,
34
CredentialConfigurationSupportedV1_0_13,
4-
CredentialRequestV1_0_13,
55
CredentialSupportedSdJwtVc,
66
} from '@sphereon/oid4vci-common';
77
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -109,7 +109,7 @@ describe('sd-jwt vc', () => {
109109
const supported = client.getCredentialsSupported('vc+sd-jwt');
110110
expect(supported).toEqual({ SdJwtCredentialId: { format: 'vc+sd-jwt', id: 'SdJwtCredentialId', vct: 'SdJwtCredentialId' } });
111111

112-
const offered = supported['SdJwtCredentialId'] as CredentialSupportedSdJwtVc;
112+
const offered = supported['SdJwtCredentialId'] as CredentialConfigurationSupportedSdJwtVcV1_0_13;
113113

114114
nock(issuerMetadata.token_endpoint as string)
115115
.post('/')
@@ -130,7 +130,7 @@ describe('sd-jwt vc', () => {
130130
.post('/')
131131
.reply(200, async (_, body) =>
132132
vcIssuer.issueCredential({
133-
credentialRequest: { ...(body as CredentialRequestV1_0_13), credential_identifier: offered.vct },
133+
credentialRequest: { ...(body as any), credential_identifier: 'SdJwtCredentialId' },
134134
credential: {
135135
vct: 'Hello',
136136
iss: 'did:example:123',
@@ -233,7 +233,7 @@ describe('sd-jwt vc', () => {
233233
.post('/')
234234
.reply(200, async (_, body) =>
235235
vcIssuer.issueCredential({
236-
credentialRequest: { ...(body as CredentialRequestV1_0_13), credential_identifier: offered.vct },
236+
credentialRequest: { ...(body as any), credential_identifier: offered.vct },
237237
credential: {
238238
vct: 'Hello',
239239
iss: 'example.com',

packages/common/lib/functions/CredentialRequestUtil.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,28 @@ export function getTypesFromRequest(credentialRequest: CredentialRequest, opts?:
1313
let types: string[] = [];
1414
if ('credential_identifier' in credentialRequest && credentialRequest.credential_identifier) {
1515
throw Error(`Cannot get types from request when it contains a credential_identifier`);
16-
} else if (credentialRequest.format === 'jwt_vc_json' || credentialRequest.format === 'jwt_vc') {
17-
types = 'types' in credentialRequest ? credentialRequest.types : [];
18-
} else if (credentialRequest.format === 'jwt_vc_json-ld' || credentialRequest.format === 'ldp_vc') {
19-
types =
20-
'credential_definition' in credentialRequest && credentialRequest.credential_definition
21-
? credentialRequest.credential_definition.types
22-
: // eslint-disable-next-line @typescript-eslint/ban-ts-comment
23-
// @ts-ignore
24-
'types' in credentialRequest.types
25-
? (credentialRequest['types' as keyof CredentialRequest] as unknown as string[])
26-
: [];
27-
} else if (credentialRequest.format === 'vc+sd-jwt') {
28-
types = 'vct' in credentialRequest ? [credentialRequest.vct as string] : [];
16+
} else if (
17+
credentialRequest.format === 'jwt_vc_json-ld' ||
18+
credentialRequest.format === 'ldp_vc' ||
19+
credentialRequest.format === 'jwt_vc' ||
20+
credentialRequest.format === 'jwt_vc_json'
21+
) {
22+
if ('credential_definition' in credentialRequest && credentialRequest.credential_definition) {
23+
types =
24+
'types' in credentialRequest.credential_definition
25+
? credentialRequest.credential_definition.types
26+
: credentialRequest.credential_definition.type;
27+
}
28+
29+
if ('type' in credentialRequest && Array.isArray(credentialRequest.type)) {
30+
types = credentialRequest.type;
31+
}
32+
33+
if ('types' in credentialRequest && Array.isArray(credentialRequest.types)) {
34+
types = credentialRequest.types;
35+
}
36+
} else if (credentialRequest.format === 'vc+sd-jwt' && 'vct' in credentialRequest) {
37+
types = [credentialRequest.vct];
2938
}
3039

3140
if (!types || types.length === 0) {

packages/common/lib/functions/IssuerMetadataUtils.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getTypesFromObject, VCI_LOG_COMMON } from '../index';
1+
import { getTypesFromObject, isW3cCredentialSupported, VCI_LOG_COMMON } from '../index';
22
import {
33
AuthorizationServerMetadata,
44
CredentialConfigurationSupported,
@@ -112,13 +112,11 @@ export function getSupportedCredential(opts?: {
112112
} else if (types) {
113113
isTypeMatch = normalizedTypes.every((type) => types.includes(type));
114114
} else {
115-
if ('credential_definition' in config) {
116-
isTypeMatch = normalizedTypes.every((type) => config.credential_definition.type?.includes(type));
117-
} else if ('type' in config && Array.isArray(config.type)) {
118-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
119-
// @ts-ignore
120-
isTypeMatch = normalizedTypes.every((type) => config.type.includes(type));
121-
} else if ('types' in config) {
115+
if (isW3cCredentialSupported(config) && 'credential_definition' in config) {
116+
isTypeMatch = normalizedTypes.every((type) => config.credential_definition.type.includes(type));
117+
} else if (isW3cCredentialSupported(config) && 'type' in config && Array.isArray(config.type)) {
118+
isTypeMatch = normalizedTypes.every((type) => (config.type as string[]).includes(type));
119+
} else if (isW3cCredentialSupported(config) && 'types' in config) {
122120
isTypeMatch = normalizedTypes.every((type) => config.types?.includes(type));
123121
}
124122
}
@@ -183,7 +181,7 @@ export function getIssuerDisplays(metadata: CredentialIssuerMetadata | IssuerMet
183181
metadata.display?.filter(
184182
(item) => !opts?.prefLocales || opts.prefLocales.length === 0 || (item.locale && opts.prefLocales.includes(item.locale)) || !item.locale,
185183
) ?? [];
186-
return matchedDisplays.sort((item) => (item.locale ? (opts?.prefLocales.indexOf(item.locale) ?? 1) : Number.MAX_VALUE));
184+
return matchedDisplays.sort((item) => (item.locale ? opts?.prefLocales.indexOf(item.locale) ?? 1 : Number.MAX_VALUE));
187185
}
188186

189187
/**

packages/common/lib/functions/TypeConversionUtils.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
import { AuthorizationDetails, CredentialOfferPayload, UniformCredentialOfferPayload, UniformCredentialOfferRequest, VCI_LOG_COMMON } from '../index';
2-
import { CredentialConfigurationSupported, CredentialDefinitionV1_0_13, CredentialOfferFormat, JsonLdIssuerCredentialDefinition } from '../types';
2+
import {
3+
CredentialConfigurationSupported,
4+
CredentialConfigurationSupportedSdJwtVcV1_0_13,
5+
CredentialDefinitionJwtVcJsonLdAndLdpVcV1_0_13,
6+
CredentialDefinitionJwtVcJsonV1_0_13,
7+
CredentialOfferFormat,
8+
CredentialsSupportedLegacy,
9+
CredentialSupportedSdJwtVc,
10+
JsonLdIssuerCredentialDefinition,
11+
} from '../types';
12+
13+
export function isW3cCredentialSupported(
14+
supported: CredentialConfigurationSupported | CredentialsSupportedLegacy,
15+
): supported is Exclude<CredentialConfigurationSupported, CredentialConfigurationSupportedSdJwtVcV1_0_13 | CredentialSupportedSdJwtVc> {
16+
return ['jwt_vc_json', 'jwt_vc_json-ld', 'ldp_vc', 'jwt_vc'].includes(supported.format);
17+
}
318

419
export const getNumberOrUndefined = (input?: string): number | undefined => {
520
return input && !isNaN(+input) ? +input : undefined;
@@ -10,14 +25,20 @@ export const getNumberOrUndefined = (input?: string): number | undefined => {
1025
* @param subject
1126
*/
1227
export function getTypesFromObject(
13-
subject: CredentialConfigurationSupported | CredentialOfferFormat | CredentialDefinitionV1_0_13 | JsonLdIssuerCredentialDefinition | string,
28+
subject:
29+
| CredentialConfigurationSupported
30+
| CredentialOfferFormat
31+
| CredentialDefinitionJwtVcJsonLdAndLdpVcV1_0_13
32+
| CredentialDefinitionJwtVcJsonV1_0_13
33+
| JsonLdIssuerCredentialDefinition
34+
| string,
1435
): string[] | undefined {
1536
if (subject === undefined) {
1637
return undefined;
1738
} else if (typeof subject === 'string') {
1839
return [subject];
19-
} else if ('credential_definition' in subject && subject.credential_definition) {
20-
return getTypesFromObject(subject.credential_definition);
40+
} else if ('credential_definition' in subject) {
41+
return getTypesFromObject(subject.credential_definition as CredentialDefinitionJwtVcJsonLdAndLdpVcV1_0_13 | CredentialDefinitionJwtVcJsonV1_0_13 | JsonLdIssuerCredentialDefinition);
2142
} else if ('types' in subject && subject.types) {
2243
return Array.isArray(subject.types) ? subject.types : [subject.types];
2344
} else if ('type' in subject && subject.type) {
@@ -77,8 +98,6 @@ export function getTypesFromCredentialSupported(
7798
) {
7899
types = getTypesFromObject(credentialSupported) ?? [];
79100
} else if (credentialSupported.format === 'vc+sd-jwt') {
80-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
81-
// @ts-ignore
82101
types = [credentialSupported.vct];
83102
}
84103

packages/common/lib/types/Generic.types.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,13 +123,17 @@ export interface ProofType {
123123
proof_signing_alg_values_supported: string[];
124124
}
125125

126+
export type ProofTypesSupported = {
127+
[key in KeyProofType]?: ProofType;
128+
};
129+
126130
export type CommonCredentialSupported = CredentialSupportedBrief &
127131
ExperimentalSubjectIssuance & {
128132
format: OID4VCICredentialFormat | string; //REQUIRED. A JSON string identifying the format of this credential, e.g. jwt_vc_json or ldp_vc.
129133
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
130134
display?: CredentialsSupportedDisplay[]; // OPTIONAL. An array of objects, where each object contains the display properties of the supported credential for a certain language
131135
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.
132-
proof_types_supported?: Record<KeyProofType, ProofType>;
136+
proof_types_supported?: ProofTypesSupported;
133137

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

0 commit comments

Comments
 (0)