@sphereon/oid4vci-common 0.10.4-unstable.98 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/functions/CredentialOfferUtil.d.ts +3 -7
  2. package/dist/functions/CredentialOfferUtil.d.ts.map +1 -1
  3. package/dist/functions/CredentialOfferUtil.js +63 -64
  4. package/dist/functions/CredentialOfferUtil.js.map +1 -1
  5. package/dist/functions/IssuerMetadataUtils.d.ts +10 -3
  6. package/dist/functions/IssuerMetadataUtils.d.ts.map +1 -1
  7. package/dist/functions/IssuerMetadataUtils.js +147 -30
  8. package/dist/functions/IssuerMetadataUtils.js.map +1 -1
  9. package/dist/functions/ProofUtil.d.ts +3 -1
  10. package/dist/functions/ProofUtil.d.ts.map +1 -1
  11. package/dist/functions/ProofUtil.js +21 -19
  12. package/dist/functions/ProofUtil.js.map +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/types/Authorization.types.d.ts +15 -1
  17. package/dist/types/Authorization.types.d.ts.map +1 -1
  18. package/dist/types/Authorization.types.js +7 -1
  19. package/dist/types/Authorization.types.js.map +1 -1
  20. package/dist/types/CredentialIssuance.types.d.ts +2 -0
  21. package/dist/types/CredentialIssuance.types.d.ts.map +1 -1
  22. package/dist/types/CredentialIssuance.types.js.map +1 -1
  23. package/dist/types/Generic.types.d.ts +1 -0
  24. package/dist/types/Generic.types.d.ts.map +1 -1
  25. package/dist/types/Generic.types.js.map +1 -1
  26. package/lib/__tests__/CredentialOfferUtil.spec.ts +19 -3
  27. package/lib/functions/CredentialOfferUtil.ts +57 -60
  28. package/lib/functions/IssuerMetadataUtils.ts +141 -38
  29. package/lib/functions/ProofUtil.ts +27 -11
  30. package/lib/index.ts +2 -4
  31. package/lib/types/Authorization.types.ts +17 -1
  32. package/lib/types/CredentialIssuance.types.ts +2 -0
  33. package/lib/types/Generic.types.ts +2 -0
  34. package/package.json +3 -3
@@ -1,3 +1,4 @@
1
+ import { CredentialDefinitionV1_0_13, CredentialOfferFormat, JsonLdIssuerCredentialDefinition, VCI_LOG_COMMON } from '../index';
1
2
  import {
2
3
  AuthorizationServerMetadata,
3
4
  CredentialConfigurationSupported,
@@ -11,27 +12,58 @@ import {
11
12
  OpenId4VCIVersion,
12
13
  } from '../types';
13
14
 
14
- export function getSupportedCredentials(options?: {
15
+ export function getSupportedCredentials(opts?: {
15
16
  issuerMetadata?: CredentialIssuerMetadata | IssuerMetadata;
16
17
  version: OpenId4VCIVersion;
17
18
  types?: string[][];
18
19
  format?: OID4VCICredentialFormat | string | (OID4VCICredentialFormat | string)[];
19
- }): Record<string, CredentialConfigurationSupportedV1_0_13> {
20
- if (options?.types && Array.isArray(options.types)) {
21
- return options.types
22
- .map((typeSet) => {
23
- return getSupportedCredential({ ...options, types: typeSet });
24
- })
25
- .reduce(
26
- (acc, result) => {
27
- Object.assign(acc, result);
28
- return acc;
29
- },
30
- {} as Record<string, CredentialConfigurationSupportedV1_0_13>,
31
- );
20
+ }): Record<string, CredentialConfigurationSupportedV1_0_13> | Array<CredentialConfigurationSupported> {
21
+ const { version = OpenId4VCIVersion.VER_1_0_13, types } = opts ?? {};
22
+ if (types && Array.isArray(types)) {
23
+ if (version < OpenId4VCIVersion.VER_1_0_13) {
24
+ return types.flatMap((typeSet) => getSupportedCredential({ ...opts, version, types: typeSet }) as Array<CredentialConfigurationSupported>);
25
+ } else {
26
+ return types
27
+ .map((typeSet) => {
28
+ return getSupportedCredential({ ...opts, version, types: typeSet });
29
+ })
30
+ .reduce(
31
+ (acc, result) => {
32
+ Object.assign(acc, result);
33
+ return acc;
34
+ },
35
+ {} as Record<string, CredentialConfigurationSupportedV1_0_13>,
36
+ );
37
+ }
38
+ }
39
+
40
+ return getSupportedCredential(opts ? { ...opts, types: undefined } : undefined);
41
+ }
42
+
43
+ export function determineVersionsFromIssuerMetadata(issuerMetadata: CredentialIssuerMetadata | IssuerMetadata): Array<OpenId4VCIVersion> {
44
+ const versions = new Set<OpenId4VCIVersion>();
45
+ if ('authorization_server' in issuerMetadata) {
46
+ versions.add(OpenId4VCIVersion.VER_1_0_11);
47
+ } else if ('authorization_servers' in issuerMetadata) {
48
+ versions.add(OpenId4VCIVersion.VER_1_0_13);
49
+ }
50
+ if (versions.size === 0) {
51
+ // The above checks where already very specific and only applicable to single versions we support, so let's skip if we encounter them
52
+ if ('credential_configurations_supported' in issuerMetadata) {
53
+ versions.add(OpenId4VCIVersion.VER_1_0_13);
54
+ } else if ('credentials_supported' in issuerMetadata) {
55
+ if (typeof issuerMetadata.credentials_supported === 'object') {
56
+ versions.add(OpenId4VCIVersion.VER_1_0_08);
57
+ } else {
58
+ versions.add(OpenId4VCIVersion.VER_1_0_09).add(OpenId4VCIVersion.VER_1_0_11);
59
+ }
60
+ }
61
+ }
62
+ if (versions.size === 0) {
63
+ versions.add(OpenId4VCIVersion.VER_UNKNOWN);
32
64
  }
33
65
 
34
- return getSupportedCredential(options ? { ...options, types: undefined } : undefined);
66
+ return Array.from(versions).sort().reverse(); // highest version first
35
67
  }
36
68
 
37
69
  export function getSupportedCredential(opts?: {
@@ -39,31 +71,82 @@ export function getSupportedCredential(opts?: {
39
71
  version: OpenId4VCIVersion;
40
72
  types?: string | string[];
41
73
  format?: OID4VCICredentialFormat | string | (OID4VCICredentialFormat | string)[];
42
- }): Record<string, CredentialConfigurationSupportedV1_0_13> {
43
- const { issuerMetadata, types, format } = opts ?? {};
74
+ }): Record<string, CredentialConfigurationSupportedV1_0_13> | Array<CredentialConfigurationSupported> {
75
+ const { issuerMetadata, types, format, version = OpenId4VCIVersion.VER_1_0_13 } = opts ?? {};
44
76
 
45
- if (!issuerMetadata || !issuerMetadata.credential_configurations_supported) {
46
- return {};
77
+ let credentialConfigurationsV11: Array<CredentialConfigurationSupported> | undefined = undefined;
78
+ let credentialConfigurationsV13: Record<string, CredentialConfigurationSupportedV1_0_13> | undefined = undefined;
79
+ if (version < OpenId4VCIVersion.VER_1_0_12 || issuerMetadata?.credentials_supported) {
80
+ if (issuerMetadata?.credentials_supported && !Array.isArray(issuerMetadata?.credentials_supported)) {
81
+ // The current code duplication and logic is such a mess, that we re-adjust the object to the proper type again
82
+ credentialConfigurationsV11 = [];
83
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
84
+ Object.entries(issuerMetadata.credentials_supported!).forEach(([id, supported]) => {
85
+ if (!supported.id) {
86
+ supported.id = id;
87
+ }
88
+ credentialConfigurationsV11?.push(supported as CredentialConfigurationSupported);
89
+ });
90
+ } else {
91
+ credentialConfigurationsV11 = (issuerMetadata?.credentials_supported as Array<CredentialConfigurationSupported>) ?? [];
92
+ }
93
+ } else {
94
+ credentialConfigurationsV13 =
95
+ (issuerMetadata?.credential_configurations_supported as Record<string, CredentialConfigurationSupportedV1_0_13>) ?? {};
96
+ }
97
+ if (!issuerMetadata || (!issuerMetadata.credential_configurations_supported && !issuerMetadata.credentials_supported)) {
98
+ VCI_LOG_COMMON.warning(`No credential issuer metadata or supported credentials found for issuer}`);
99
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
100
+ return version < OpenId4VCIVersion.VER_1_0_13 ? credentialConfigurationsV11! : credentialConfigurationsV13!;
47
101
  }
48
102
 
49
- const credentialConfigurations: Record<string, CredentialConfigurationSupportedV1_0_13> =
50
- issuerMetadata.credential_configurations_supported as Record<string, CredentialConfigurationSupportedV1_0_13>;
51
103
  const normalizedTypes: string[] = Array.isArray(types) ? types : types ? [types] : [];
52
104
  const normalizedFormats: string[] = Array.isArray(format) ? format : format ? [format] : [];
53
105
 
54
- return Object.entries(credentialConfigurations).reduce(
55
- (filteredConfigs, [id, config]) => {
56
- const isTypeMatch = normalizedTypes.length === 0 || normalizedTypes.some((type) => config.credential_definition.type?.includes(type));
57
- const isFormatMatch = normalizedFormats.length === 0 || normalizedFormats.includes(config.format);
58
-
59
- if (isTypeMatch && isFormatMatch) {
60
- filteredConfigs[id] = config;
106
+ function filterMatchingConfig(config: CredentialConfigurationSupported): CredentialConfigurationSupported | undefined {
107
+ let isTypeMatch = normalizedTypes.length === 0;
108
+ const types = getTypesFromObject(config);
109
+ if (!isTypeMatch) {
110
+ if (normalizedTypes.length === 1 && config.id === normalizedTypes[0]) {
111
+ isTypeMatch = true;
112
+ } else if (types) {
113
+ isTypeMatch = normalizedTypes.every((type) => types.includes(type));
114
+ } 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) {
122
+ isTypeMatch = normalizedTypes.every((type) => config.types?.includes(type));
123
+ }
61
124
  }
125
+ }
126
+
127
+ const isFormatMatch = normalizedFormats.length === 0 || normalizedFormats.includes(config.format);
128
+
129
+ return isTypeMatch && isFormatMatch ? config : undefined;
130
+ }
62
131
 
63
- return filteredConfigs;
64
- },
65
- {} as Record<string, CredentialConfigurationSupportedV1_0_13>,
66
- );
132
+ if (credentialConfigurationsV13) {
133
+ return Object.entries(credentialConfigurationsV13).reduce(
134
+ (filteredConfigs, [id, config]) => {
135
+ if (filterMatchingConfig(config)) {
136
+ filteredConfigs[id] = config;
137
+ // Added to enable support < 13. We basically assign the
138
+ if (!config.id) {
139
+ config.id = id;
140
+ }
141
+ }
142
+ return filteredConfigs;
143
+ },
144
+ {} as Record<string, CredentialConfigurationSupportedV1_0_13>,
145
+ );
146
+ } else if (credentialConfigurationsV11) {
147
+ return credentialConfigurationsV11.filter((config) => filterMatchingConfig(config));
148
+ }
149
+ throw Error(`Either < v11 configurations or V13 configurations should have been filtered at this point`);
67
150
  }
68
151
 
69
152
  export function getTypesFromCredentialSupported(
@@ -77,12 +160,7 @@ export function getTypesFromCredentialSupported(
77
160
  credentialSupported.format === 'jwt_vc_json-ld' ||
78
161
  credentialSupported.format === 'ldp_vc'
79
162
  ) {
80
- types =
81
- (credentialSupported.types
82
- ? (credentialSupported.types as string[])
83
- : 'credential_definition' in credentialSupported
84
- ? credentialSupported.credential_definition?.type
85
- : []) ?? [];
163
+ types = getTypesFromObject(credentialSupported) ?? [];
86
164
  } else if (credentialSupported.format === 'vc+sd-jwt') {
87
165
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
88
166
  // @ts-ignore
@@ -152,3 +230,28 @@ export function getIssuerName(
152
230
  }
153
231
  return url;
154
232
  }
233
+
234
+ /**
235
+ * The specs had many places where types could be expressed. This method ensures we get them in any way possible
236
+ * @param subject
237
+ */
238
+ export function getTypesFromObject(
239
+ subject: CredentialConfigurationSupported | CredentialOfferFormat | CredentialDefinitionV1_0_13 | JsonLdIssuerCredentialDefinition | string,
240
+ ): string[] | undefined {
241
+ if (subject === undefined) {
242
+ return undefined;
243
+ } else if (typeof subject === 'string') {
244
+ return [subject];
245
+ } else if ('credential_definition' in subject && subject.credential_definition) {
246
+ return getTypesFromObject(subject.credential_definition);
247
+ } else if ('types' in subject && subject.types) {
248
+ return Array.isArray(subject.types) ? subject.types : [subject.types];
249
+ } else if ('type' in subject && subject.type) {
250
+ return Array.isArray(subject.type) ? subject.type : [subject.type];
251
+ } else if ('vct' in subject && subject.vct) {
252
+ return [subject.vct];
253
+ }
254
+
255
+ VCI_LOG_COMMON.warning('Could not deduce credential types. Probably a failure down the line will happen!');
256
+ return undefined;
257
+ }
@@ -1,7 +1,7 @@
1
1
  import Debug from 'debug';
2
2
  import jwtDecode from 'jwt-decode';
3
3
 
4
- import { VCI_LOG_COMMON } from '..';
4
+ import { PoPMode, VCI_LOG_COMMON } from '..';
5
5
  import {
6
6
  BAD_PARAMS,
7
7
  BaseJWK,
@@ -37,6 +37,7 @@ const debug = Debug('sphereon:openid4vci:common');
37
37
  * - Optional, clientId of the party requesting the credential
38
38
  */
39
39
  export const createProofOfPossession = async <DIDDoc>(
40
+ popMode: PoPMode,
40
41
  callbacks: ProofOfPossessionCallbacks<DIDDoc>,
41
42
  jwtProps?: JwtProps,
42
43
  existingJwt?: Jwt,
@@ -46,7 +47,7 @@ export const createProofOfPossession = async <DIDDoc>(
46
47
  throw new Error(BAD_PARAMS);
47
48
  }
48
49
 
49
- const signerArgs = createJWT(jwtProps, existingJwt);
50
+ const signerArgs = createJWT(popMode, jwtProps, existingJwt);
50
51
  const jwt = await callbacks.signCallback(signerArgs, signerArgs.header.kid);
51
52
  const proof = {
52
53
  proof_type: 'jwt',
@@ -114,6 +115,7 @@ export interface JwtProps {
114
115
  typ?: Typ;
115
116
  kid?: string;
116
117
  jwk?: JWK;
118
+ aud?: string | string[];
117
119
  issuer?: string;
118
120
  clientId?: string;
119
121
  alg?: string;
@@ -121,9 +123,16 @@ export interface JwtProps {
121
123
  nonce?: string;
122
124
  }
123
125
 
124
- const createJWT = (jwtProps?: JwtProps, existingJwt?: Jwt): Jwt => {
125
- const aud = getJwtProperty<string | string[]>('aud', true, jwtProps?.issuer, existingJwt?.payload?.aud);
126
- const iss = getJwtProperty<string>('iss', false, jwtProps?.clientId, existingJwt?.payload?.iss);
126
+ const createJWT = (mode: PoPMode, jwtProps?: JwtProps, existingJwt?: Jwt): Jwt => {
127
+ const aud =
128
+ mode === 'pop'
129
+ ? getJwtProperty<string | string[]>('aud', true, jwtProps?.issuer, existingJwt?.payload?.aud)
130
+ : getJwtProperty<string | string[]>('aud', false, jwtProps?.aud, existingJwt?.payload?.aud);
131
+ const iss =
132
+ // mode === 'pop'
133
+ getJwtProperty<string>('iss', false, jwtProps?.clientId, existingJwt?.payload?.iss)
134
+ // : getJwtProperty<string>('iss', false, jwtProps?.issuer, existingJwt?.payload?.iss);
135
+ const client_id = mode === 'jwt' ? getJwtProperty<string>('client_id', false, jwtProps?.clientId, existingJwt?.payload?.client_id) : undefined;
127
136
  const jti = getJwtProperty<string>('jti', false, jwtProps?.jti, existingJwt?.payload?.jti);
128
137
  const typ = getJwtProperty<string>('typ', true, jwtProps?.typ, existingJwt?.header?.typ, 'jwt');
129
138
  const nonce = getJwtProperty<string>('nonce', false, jwtProps?.nonce, existingJwt?.payload?.nonce); // Officially this is required, but some implementations don't have it
@@ -134,19 +143,20 @@ const createJWT = (jwtProps?: JwtProps, existingJwt?: Jwt): Jwt => {
134
143
  const jwt: Partial<Jwt> = existingJwt ? existingJwt : {};
135
144
  const now = +new Date();
136
145
  const jwtPayload: Partial<JWTPayload> = {
137
- aud,
146
+ ...(aud && { aud }),
138
147
  iat: jwt.payload?.iat ?? Math.round(now / 1000 - 60), // Let's ensure we subtract 60 seconds for potential time offsets
139
148
  exp: jwt.payload?.exp ?? Math.round(now / 1000 + 10 * 60),
140
149
  nonce,
141
- ...(iss ? { iss } : {}),
142
- ...(jti ? { jti } : {}),
150
+ ...(client_id && { client_id }),
151
+ ...(iss && { iss }),
152
+ ...(jti && { jti }),
143
153
  };
144
154
 
145
155
  const jwtHeader: JWTHeader = {
146
156
  typ,
147
157
  alg,
148
- kid,
149
- jwk,
158
+ ...(kid && { kid }),
159
+ ...(jwk && { jwk }),
150
160
  };
151
161
  return {
152
162
  payload: { ...jwt.payload, ...jwtPayload },
@@ -154,7 +164,13 @@ const createJWT = (jwtProps?: JwtProps, existingJwt?: Jwt): Jwt => {
154
164
  };
155
165
  };
156
166
 
157
- const getJwtProperty = <T>(propertyName: string, required: boolean, option?: string | JWK, jwtProperty?: T, defaultValue?: T): T | undefined => {
167
+ const getJwtProperty = <T>(
168
+ propertyName: string,
169
+ required: boolean,
170
+ option?: string | string[] | JWK,
171
+ jwtProperty?: T,
172
+ defaultValue?: T,
173
+ ): T | undefined => {
158
174
  if (typeof option === 'string' && option && jwtProperty && option !== jwtProperty) {
159
175
  throw Error(`Cannot have a property '${propertyName}' with value '${option}' and different JWT value '${jwtProperty}' at the same time`);
160
176
  }
package/lib/index.ts CHANGED
@@ -1,9 +1,7 @@
1
- import { Loggers, LogMethod } from '@sphereon/ssi-types';
1
+ import { Loggers } from '@sphereon/ssi-types';
2
2
 
3
3
  export const VCI_LOGGERS = Loggers.DEFAULT;
4
- export const VCI_LOG_COMMON = VCI_LOGGERS.options('sphereon:oid4vci:common', { methods: [LogMethod.EVENT, LogMethod.DEBUG_PKG] }).get(
5
- 'sphereon:oid4vci:common',
6
- );
4
+ export const VCI_LOG_COMMON = VCI_LOGGERS.get('sphereon:oid4vci:common');
7
5
 
8
6
  export * from './functions';
9
7
  export * from './types';
@@ -1,4 +1,4 @@
1
- import { CredentialOfferPayload, UniformCredentialOffer } from './CredentialIssuance.types';
1
+ import { CredentialOfferPayload, ProofOfPossessionCallbacks, UniformCredentialOffer } from './CredentialIssuance.types';
2
2
  import {
3
3
  ErrorResponse,
4
4
  IssuerCredentialSubject,
@@ -260,6 +260,21 @@ export interface PKCEOpts {
260
260
  codeVerifier?: string;
261
261
  }
262
262
 
263
+ export enum CreateRequestObjectMode {
264
+ NONE,
265
+ REQUEST_OBJECT,
266
+ REQUEST_URI,
267
+ }
268
+
269
+ export type RequestObjectOpts = {
270
+ requestObjectMode?: CreateRequestObjectMode;
271
+ signCallbacks?: ProofOfPossessionCallbacks<never>;
272
+ clientMetadata?: Record<string, any>; // TODO: Merge SIOP/OID4VP
273
+ iss?: string;
274
+ jwksUri?: string;
275
+ kid?: string;
276
+ };
277
+
263
278
  export interface AuthorizationRequestOpts {
264
279
  clientId?: string;
265
280
  pkce?: PKCEOpts;
@@ -267,6 +282,7 @@ export interface AuthorizationRequestOpts {
267
282
  authorizationDetails?: AuthorizationDetails | AuthorizationDetails[];
268
283
  redirectUri?: string;
269
284
  scope?: string;
285
+ requestObjectOpts?: RequestObjectOpts;
270
286
  }
271
287
 
272
288
  export interface AuthorizationResponse {
@@ -27,6 +27,7 @@ export interface CredentialOfferRequestWithBaseUrl extends UniformCredentialOffe
27
27
  txCode?: TxCode;
28
28
  issuerState?: string;
29
29
  preAuthorizedCode?: string;
30
+ userPinRequired: boolean;
30
31
  }
31
32
 
32
33
  export type CredentialOffer = CredentialOfferV1_0_09 | CredentialOfferV1_0_11 | CredentialOfferV1_0_13;
@@ -198,6 +199,7 @@ export interface JWTPayload {
198
199
  nonce?: string; // REQUIRED (string). The value type of this claim MUST be a string, where the value is a c_nonce provided by the credential issuer. //TODO: Marked as required not present in NGI flow
199
200
  jti?: string; // A new nonce chosen by the wallet. Used to prevent replay
200
201
  exp?: number; // Not longer than 5 minutes
202
+ client_id?: string; // (string). The value of this claim MUST be the client_id of the client making the credential request.
201
203
  [s: string]: unknown;
202
204
  }
203
205
 
@@ -17,6 +17,8 @@ import {
17
17
  export type InputCharSet = 'numeric' | 'text';
18
18
  export type KeyProofType = 'jwt' | 'cwt' | 'ldp_vp';
19
19
 
20
+ export type PoPMode = 'pop' | 'jwt'; // Proof of posession, or regular JWT
21
+
20
22
  /**
21
23
  * Important Note: please be aware that these Common interfaces are based on versions v1_0.11 and v1_0.09
22
24
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sphereon/oid4vci-common",
3
- "version": "0.10.4-unstable.98+b0ade3b",
3
+ "version": "0.12.0",
4
4
  "description": "OpenID 4 Verifiable Credential Issuance Common Types",
5
5
  "source": "lib/index.ts",
6
6
  "main": "dist/index.js",
@@ -10,7 +10,7 @@
10
10
  "build:clean": "tsc --build --clean && tsc --build"
11
11
  },
12
12
  "dependencies": {
13
- "@sphereon/ssi-types": "0.24.1-unstable.129",
13
+ "@sphereon/ssi-types": "0.25.1-unstable.87",
14
14
  "cross-fetch": "^3.1.8",
15
15
  "jwt-decode": "^3.1.2",
16
16
  "sha.js": "^2.4.11",
@@ -49,5 +49,5 @@
49
49
  "publishConfig": {
50
50
  "access": "public"
51
51
  },
52
- "gitHead": "b0ade3b9482bc6be469b46e8ce886359e3ccdf82"
52
+ "gitHead": "a76c382a36e38428391c9e8d1a6b67e477fc659a"
53
53
  }