@sphereon/oid4vci-client 0.8.2-unstable.49 → 0.8.2-unstable.52

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.
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createPKCEOpts = void 0;
3
+ exports.generateMissingPKCEOpts = void 0;
4
4
  const oid4vci_common_1 = require("@sphereon/oid4vci-common");
5
- const createPKCEOpts = (pkce) => {
5
+ const generateMissingPKCEOpts = (pkce) => {
6
6
  if (pkce.disabled) {
7
7
  return pkce;
8
8
  }
@@ -18,5 +18,5 @@ const createPKCEOpts = (pkce) => {
18
18
  }
19
19
  return pkce;
20
20
  };
21
- exports.createPKCEOpts = createPKCEOpts;
21
+ exports.generateMissingPKCEOpts = generateMissingPKCEOpts;
22
22
  //# sourceMappingURL=AuthorizationUtil.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"AuthorizationUtil.js","sourceRoot":"","sources":["../../lib/functions/AuthorizationUtil.ts"],"names":[],"mappings":";;;AAAA,6DAAmI;AAI5H,MAAM,cAAc,GAAG,CAAC,IAAc,EAAE,EAAE;IAC/C,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QAClB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,mBAAmB,EAAE,CAAC;QAC9B,IAAI,CAAC,mBAAmB,GAAG,oCAAmB,CAAC,IAAI,CAAC;IACtD,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;QACvB,IAAI,CAAC,YAAY,GAAG,IAAA,qCAAoB,GAAE,CAAC;IAC7C,CAAC;IACD,IAAA,wCAAuB,EAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAC3C,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;QACxB,IAAI,CAAC,aAAa,GAAG,IAAA,oCAAmB,EAAC,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,mBAAmB,CAAC,CAAC;IACxF,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAfW,QAAA,cAAc,kBAezB"}
1
+ {"version":3,"file":"AuthorizationUtil.js","sourceRoot":"","sources":["../../lib/functions/AuthorizationUtil.ts"],"names":[],"mappings":";;;AAAA,6DAA6I;AAEtI,MAAM,uBAAuB,GAAG,CAAC,IAAc,EAAE,EAAE;IACxD,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QAClB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,mBAAmB,EAAE,CAAC;QAC9B,IAAI,CAAC,mBAAmB,GAAG,oCAAmB,CAAC,IAAI,CAAC;IACtD,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;QACvB,IAAI,CAAC,YAAY,GAAG,IAAA,qCAAoB,GAAE,CAAC;IAC7C,CAAC;IACD,IAAA,wCAAuB,EAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAC3C,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;QACxB,IAAI,CAAC,aAAa,GAAG,IAAA,oCAAmB,EAAC,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,mBAAmB,CAAC,CAAC;IACxF,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAfW,QAAA,uBAAuB,2BAelC"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './AccessTokenClient';
2
+ export * from './CredentialRequestClient';
2
3
  export * from './CredentialOfferClient';
3
4
  export * from './CredentialRequestClient';
4
5
  export * from './CredentialRequestClientBuilder';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,kCAAkC,CAAC;AACjD,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,4BAA4B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,kCAAkC,CAAC;AACjD,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,4BAA4B,CAAC"}
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./AccessTokenClient"), exports);
18
+ __exportStar(require("./CredentialRequestClient"), exports);
18
19
  __exportStar(require("./CredentialOfferClient"), exports);
19
20
  __exportStar(require("./CredentialRequestClient"), exports);
20
21
  __exportStar(require("./CredentialRequestClientBuilder"), exports);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,sDAAoC;AACpC,0DAAwC;AACxC,4DAA0C;AAC1C,mEAAiD;AACjD,8CAA4B;AAC5B,mDAAiC;AACjC,qDAAmC;AACnC,6DAA2C"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,sDAAoC;AACpC,4DAA0C;AAC1C,0DAAwC;AACxC,4DAA0C;AAC1C,mEAAiD;AACjD,8CAA4B;AAC5B,mDAAiC;AACjC,qDAAmC;AACnC,6DAA2C"}
@@ -1,49 +1 @@
1
- import { CodeChallengeMethod } from '@sphereon/oid4vci-common';
2
- import { CredentialFormat } from '@sphereon/ssi-types';
3
- export interface AuthDetails {
4
- type: 'openid_credential' | string;
5
- locations?: string | string[];
6
- format: CredentialFormat | CredentialFormat[];
7
- [s: string]: unknown;
8
- }
9
- /**
10
- * Determinse whether PAR should be used when supported
11
- *
12
- * REQUIRE: Require PAR, if AS does not support it throw an error
13
- * AUTO: Use PAR is the AS supports it, otherwise construct a reqular URI,
14
- * NEVER: Do not use PAR even if the AS supports it (not recommended)
15
- */
16
- export declare enum PARMode {
17
- REQUIRE = 0,
18
- AUTO = 1,
19
- NEVER = 2
20
- }
21
- export interface AuthRequestOpts {
22
- pkce?: PKCEOpts;
23
- parMode?: PARMode;
24
- authorizationDetails?: AuthDetails | AuthDetails[];
25
- redirectUri: string;
26
- scope?: string;
27
- }
28
- /**
29
- * Optional options to provide PKCE params like code verifier and challenge yourself, or to disable PKCE altogether. If not provide PKCE will still be used! If individual params are not provide, they will be generated/calculated
30
- */
31
- export interface PKCEOpts {
32
- /**
33
- * PKCE is enabled by default even if you do not provide these options. Set this to true to disable PKCE
34
- */
35
- disabled?: boolean;
36
- /**
37
- * Provide a code_challenge, otherwise it will be calculated using the code_verifier and method
38
- */
39
- codeChallenge?: string;
40
- /**
41
- * The code_challenge_method, should always by S256
42
- */
43
- codeChallengeMethod?: CodeChallengeMethod;
44
- /**
45
- * Provide a code_verifier, otherwise it will be generated
46
- */
47
- codeVerifier?: string;
48
- }
49
1
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../lib/types/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEvD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,mBAAmB,GAAG,MAAM,CAAC;IACnC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC9B,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,EAAE,CAAC;IAE9C,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACtB;AAED;;;;;;GAMG;AACH,oBAAY,OAAO;IACjB,OAAO,IAAA;IACP,IAAI,IAAA;IACJ,KAAK,IAAA;CACN;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,oBAAoB,CAAC,EAAE,WAAW,GAAG,WAAW,EAAE,CAAC;IACnD,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;OAEG;IACH,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;IAE1C;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../lib/types/index.ts"],"names":[],"mappings":""}
@@ -1,17 +1,2 @@
1
1
  "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PARMode = void 0;
4
- /**
5
- * Determinse whether PAR should be used when supported
6
- *
7
- * REQUIRE: Require PAR, if AS does not support it throw an error
8
- * AUTO: Use PAR is the AS supports it, otherwise construct a reqular URI,
9
- * NEVER: Do not use PAR even if the AS supports it (not recommended)
10
- */
11
- var PARMode;
12
- (function (PARMode) {
13
- PARMode[PARMode["REQUIRE"] = 0] = "REQUIRE";
14
- PARMode[PARMode["AUTO"] = 1] = "AUTO";
15
- PARMode[PARMode["NEVER"] = 2] = "NEVER";
16
- })(PARMode || (exports.PARMode = PARMode = {}));
17
2
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../lib/types/index.ts"],"names":[],"mappings":";;;AAWA;;;;;;GAMG;AACH,IAAY,OAIX;AAJD,WAAY,OAAO;IACjB,2CAAO,CAAA;IACP,qCAAI,CAAA;IACJ,uCAAK,CAAA;AACP,CAAC,EAJW,OAAO,uBAAP,OAAO,QAIlB"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../lib/types/index.ts"],"names":[],"mappings":""}
@@ -0,0 +1,149 @@
1
+ import {
2
+ AuthorizationDetails,
3
+ AuthorizationRequestOpts,
4
+ CodeChallengeMethod,
5
+ convertJsonToURI,
6
+ CredentialOfferRequestWithBaseUrl,
7
+ CredentialSupported,
8
+ EndpointMetadataResult,
9
+ JsonURIMode,
10
+ PARMode,
11
+ PKCEOpts,
12
+ PushedAuthorizationResponse,
13
+ ResponseType,
14
+ } from '@sphereon/oid4vci-common';
15
+ import { formPost } from '@sphereon/oid4vci-common';
16
+
17
+ export const createAuthorizationRequestUrl = async ({
18
+ pkce,
19
+ endpointMetadata,
20
+ authorizationRequest,
21
+ credentialOffer,
22
+ credentialsSupported,
23
+ }: {
24
+ pkce: PKCEOpts;
25
+ endpointMetadata: EndpointMetadataResult;
26
+ authorizationRequest: AuthorizationRequestOpts;
27
+ credentialOffer?: CredentialOfferRequestWithBaseUrl;
28
+ credentialsSupported?: CredentialSupported[];
29
+ }): Promise<string> => {
30
+ const { redirectUri } = authorizationRequest;
31
+ let { scope, authorizationDetails } = authorizationRequest;
32
+ const parMode = endpointMetadata?.credentialIssuerMetadata?.require_pushed_authorization_requests
33
+ ? PARMode.REQUIRE
34
+ : authorizationRequest.parMode ?? PARMode.AUTO;
35
+ // Scope and authorization_details can be used in the same authorization request
36
+ // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param
37
+ if (!scope && !authorizationDetails) {
38
+ if (!credentialOffer) {
39
+ throw Error('Please provide a scope or authorization_details if no credential offer is present');
40
+ }
41
+ const creds = credentialOffer.credential_offer.credentials;
42
+
43
+ // FIXME: complains about VCT for sd-jwt
44
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
45
+ // @ts-ignore
46
+ authorizationDetails = creds
47
+ .flatMap((cred) => (typeof cred === 'string' ? credentialsSupported : (cred as CredentialSupported)))
48
+ .filter((cred) => !!cred)
49
+ .map((cred) => {
50
+ return {
51
+ ...cred,
52
+ type: 'openid_credential',
53
+ locations: [endpointMetadata.issuer],
54
+
55
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
56
+ // @ts-ignore
57
+ format: cred!.format,
58
+ } satisfies AuthorizationDetails;
59
+ });
60
+ if (!authorizationDetails || authorizationDetails.length === 0) {
61
+ throw Error(`Could not create authorization details from credential offer. Please pass in explicit details`);
62
+ }
63
+ }
64
+ if (!endpointMetadata?.authorization_endpoint) {
65
+ throw Error('Server metadata does not contain authorization endpoint');
66
+ }
67
+ const parEndpoint = endpointMetadata.credentialIssuerMetadata?.pushed_authorization_request_endpoint;
68
+
69
+ // add 'openid' scope if not present
70
+ if (!scope?.includes('openid')) {
71
+ scope = ['openid', scope].filter((s) => !!s).join(' ');
72
+ }
73
+
74
+ let queryObj: { [key: string]: string } | PushedAuthorizationResponse = {
75
+ response_type: ResponseType.AUTH_CODE,
76
+ ...(!pkce.disabled && {
77
+ code_challenge_method: pkce.codeChallengeMethod ?? CodeChallengeMethod.S256,
78
+ code_challenge: pkce.codeChallenge,
79
+ }),
80
+ authorization_details: JSON.stringify(handleAuthorizationDetails(endpointMetadata, authorizationDetails)),
81
+ redirect_uri: redirectUri,
82
+ scope: scope,
83
+ };
84
+
85
+ if (authorizationRequest.clientId) {
86
+ queryObj['client_id'] = authorizationRequest.clientId;
87
+ }
88
+
89
+ if (credentialOffer?.issuerState) {
90
+ queryObj['issuer_state'] = credentialOffer.issuerState;
91
+ }
92
+ if (!parEndpoint && parMode === PARMode.REQUIRE) {
93
+ throw Error(`PAR mode is set to required by Authorization Server does not support PAR!`);
94
+ } else if (parEndpoint && parMode !== PARMode.NEVER) {
95
+ const parResponse = await formPost<PushedAuthorizationResponse>(parEndpoint, new URLSearchParams(queryObj));
96
+ if (parResponse.errorBody || !parResponse.successBody) {
97
+ throw Error(`PAR error`);
98
+ }
99
+ queryObj = { request_uri: parResponse.successBody.request_uri };
100
+ }
101
+
102
+ const url = convertJsonToURI(queryObj, {
103
+ baseUrl: endpointMetadata.authorization_endpoint,
104
+ uriTypeProperties: ['request_uri', 'redirect_uri', 'scope', 'authorization_details', 'issuer_state'],
105
+ mode: JsonURIMode.X_FORM_WWW_URLENCODED,
106
+ // We do not add the version here, as this always needs to be form encoded
107
+ });
108
+ console.log(`Authorization Request URL: ${url}`);
109
+ return url;
110
+ };
111
+
112
+ const handleAuthorizationDetails = (
113
+ endpointMetadata: EndpointMetadataResult,
114
+ authorizationDetails?: AuthorizationDetails | AuthorizationDetails[],
115
+ ): AuthorizationDetails | AuthorizationDetails[] | undefined => {
116
+ if (authorizationDetails) {
117
+ if (typeof authorizationDetails === 'string') {
118
+ // backwards compat for older versions of the lib
119
+ return authorizationDetails;
120
+ }
121
+ if (Array.isArray(authorizationDetails)) {
122
+ return authorizationDetails
123
+ .filter((value) => typeof value !== 'string')
124
+ .map((value) => handleLocations(endpointMetadata, typeof value === 'string' ? value : { ...value }));
125
+ } else {
126
+ return handleLocations(endpointMetadata, { ...authorizationDetails });
127
+ }
128
+ }
129
+ return authorizationDetails;
130
+ };
131
+
132
+ const handleLocations = (endpointMetadata: EndpointMetadataResult, authorizationDetails: AuthorizationDetails) => {
133
+ if (typeof authorizationDetails === 'string') {
134
+ // backwards compat for older versions of the lib
135
+ return authorizationDetails;
136
+ }
137
+ if (authorizationDetails && (endpointMetadata.credentialIssuerMetadata?.authorization_server || endpointMetadata.authorization_endpoint)) {
138
+ if (authorizationDetails.locations) {
139
+ if (Array.isArray(authorizationDetails.locations)) {
140
+ (authorizationDetails.locations as string[]).push(endpointMetadata.issuer);
141
+ } else {
142
+ authorizationDetails.locations = [authorizationDetails.locations as string, endpointMetadata.issuer];
143
+ }
144
+ } else {
145
+ authorizationDetails.locations = [endpointMetadata.issuer];
146
+ }
147
+ }
148
+ return authorizationDetails;
149
+ };
@@ -5,6 +5,7 @@ import {
5
5
  CredentialOfferRequestWithBaseUrl,
6
6
  CredentialOfferV1_0_11,
7
7
  determineSpecVersionFromURI,
8
+ getClientIdFromCredentialOfferPayload,
8
9
  OpenId4VCIVersion,
9
10
  toUniformCredentialOfferRequest,
10
11
  } from '@sphereon/oid4vci-common';
@@ -43,6 +44,7 @@ export class CredentialOfferClient {
43
44
  throw Error('Either a credential_offer or credential_offer_uri should be present in ' + uri);
44
45
  }
45
46
  }
47
+ const clientId = getClientIdFromCredentialOfferPayload(credentialOffer?.credential_offer);
46
48
  const request = await toUniformCredentialOfferRequest(credentialOffer, {
47
49
  ...opts,
48
50
  version,
@@ -53,6 +55,7 @@ export class CredentialOfferClient {
53
55
  return {
54
56
  scheme,
55
57
  baseUrl,
58
+ clientId,
56
59
  ...request,
57
60
  ...(grants?.authorization_code?.issuer_state && { issuerState: grants.authorization_code.issuer_state }),
58
61
  ...(grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.['pre-authorized_code'] && {
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  AccessTokenResponse,
3
3
  Alg,
4
+ AuthorizationRequestOpts,
4
5
  AuthzFlowType,
5
6
  CodeChallengeMethod,
6
7
  CredentialOfferPayloadV1_0_08,
@@ -11,26 +12,23 @@ import {
11
12
  getIssuerFromCredentialOfferPayload,
12
13
  getSupportedCredentials,
13
14
  getTypesFromCredentialSupported,
14
- JsonURIMode,
15
15
  JWK,
16
16
  KID_JWK_X5C_ERROR,
17
17
  OID4VCICredentialFormat,
18
18
  OpenId4VCIVersion,
19
+ PKCEOpts,
19
20
  ProofOfPossessionCallbacks,
20
- PushedAuthorizationResponse,
21
- ResponseType,
22
21
  } from '@sphereon/oid4vci-common';
23
22
  import { CredentialFormat } from '@sphereon/ssi-types';
24
23
  import Debug from 'debug';
25
24
 
26
25
  import { AccessTokenClient } from './AccessTokenClient';
26
+ import { createAuthorizationRequestUrl } from './AuthorizationCodeClient';
27
27
  import { CredentialOfferClient } from './CredentialOfferClient';
28
28
  import { CredentialRequestClientBuilder } from './CredentialRequestClientBuilder';
29
29
  import { MetadataClient } from './MetadataClient';
30
30
  import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder';
31
- import { convertJsonToURI, formPost } from './functions';
32
- import { createPKCEOpts } from './functions/AuthorizationUtil';
33
- import { AuthDetails, AuthRequestOpts, PARMode, PKCEOpts } from './types';
31
+ import { generateMissingPKCEOpts } from './functions/AuthorizationUtil';
34
32
 
35
33
  const debug = Debug('sphereon:oid4vci');
36
34
 
@@ -44,6 +42,9 @@ export class OpenID4VCIClient {
44
42
  private _endpointMetadata: EndpointMetadataResult | undefined;
45
43
  private _accessTokenResponse: AccessTokenResponse | undefined;
46
44
  private _pkce: PKCEOpts = { disabled: false, codeChallengeMethod: CodeChallengeMethod.S256 };
45
+ private _authorizationRequestOpts?: AuthorizationRequestOpts;
46
+
47
+ private _authorizationURL?: string;
47
48
 
48
49
  private constructor({
49
50
  credentialOffer,
@@ -51,12 +52,16 @@ export class OpenID4VCIClient {
51
52
  kid,
52
53
  alg,
53
54
  credentialIssuer,
55
+ pkce,
56
+ authorizationRequest,
54
57
  }: {
55
58
  credentialOffer?: CredentialOfferRequestWithBaseUrl;
56
59
  kid?: string;
57
60
  alg?: Alg | string;
58
61
  clientId?: string;
59
62
  credentialIssuer?: string;
63
+ pkce?: PKCEOpts;
64
+ authorizationRequest?: AuthorizationRequestOpts; // Can be provided here, or when manually calling createAuthorizationUrl
60
65
  }) {
61
66
  this._credentialOffer = credentialOffer;
62
67
  const issuer = credentialIssuer ?? (credentialOffer ? getIssuerFromCredentialOfferPayload(credentialOffer.credential_offer) : undefined);
@@ -67,6 +72,9 @@ export class OpenID4VCIClient {
67
72
  this._kid = kid;
68
73
  this._alg = alg;
69
74
  this._clientId = clientId;
75
+ this._pkce = { ...this._pkce, ...pkce };
76
+ this._authorizationRequestOpts = authorizationRequest;
77
+ this.syncAuthorizationRequestOpts(authorizationRequest);
70
78
  }
71
79
 
72
80
  public static async fromCredentialIssuer({
@@ -75,17 +83,33 @@ export class OpenID4VCIClient {
75
83
  retrieveServerMetadata,
76
84
  clientId,
77
85
  credentialIssuer,
86
+ pkce,
87
+ authorizationRequest,
88
+ createAuthorizationRequestURL,
78
89
  }: {
79
90
  credentialIssuer: string;
80
91
  kid?: string;
81
92
  alg?: Alg | string;
82
93
  retrieveServerMetadata?: boolean;
83
94
  clientId?: string;
95
+ createAuthorizationRequestURL?: boolean;
96
+ authorizationRequest?: AuthorizationRequestOpts; // Can be provided here, or when manually calling createAuthorizationUrl
97
+ pkce?: PKCEOpts;
84
98
  }) {
85
- const client = new OpenID4VCIClient({ kid, alg, clientId, credentialIssuer });
99
+ const client = new OpenID4VCIClient({
100
+ kid,
101
+ alg,
102
+ clientId: clientId ?? authorizationRequest?.clientId,
103
+ credentialIssuer,
104
+ pkce,
105
+ authorizationRequest,
106
+ });
86
107
  if (retrieveServerMetadata === undefined || retrieveServerMetadata) {
87
108
  await client.retrieveServerMetadata();
88
109
  }
110
+ if (createAuthorizationRequestURL === undefined || createAuthorizationRequestURL) {
111
+ await client.createAuthorizationRequestUrl({ authorizationRequest, pkce });
112
+ }
89
113
  return client;
90
114
  }
91
115
 
@@ -95,28 +119,74 @@ export class OpenID4VCIClient {
95
119
  alg,
96
120
  retrieveServerMetadata,
97
121
  clientId,
122
+ pkce,
123
+ createAuthorizationRequestURL,
124
+ authorizationRequest,
98
125
  resolveOfferUri,
99
126
  }: {
100
127
  uri: string;
101
128
  kid?: string;
102
129
  alg?: Alg | string;
103
130
  retrieveServerMetadata?: boolean;
131
+ createAuthorizationRequestURL?: boolean;
104
132
  resolveOfferUri?: boolean;
133
+ pkce?: PKCEOpts;
105
134
  clientId?: string;
135
+ authorizationRequest?: AuthorizationRequestOpts; // Can be provided here, or when manually calling createAuthorizationUrl
106
136
  }): Promise<OpenID4VCIClient> {
137
+ const credentialOfferClient = await CredentialOfferClient.fromURI(uri, { resolve: resolveOfferUri });
107
138
  const client = new OpenID4VCIClient({
108
- credentialOffer: await CredentialOfferClient.fromURI(uri, { resolve: resolveOfferUri }),
139
+ credentialOffer: credentialOfferClient,
109
140
  kid,
110
141
  alg,
111
- clientId,
142
+ clientId: clientId ?? authorizationRequest?.clientId ?? credentialOfferClient.clientId,
143
+ pkce,
144
+ authorizationRequest,
112
145
  });
113
146
 
114
147
  if (retrieveServerMetadata === undefined || retrieveServerMetadata) {
115
148
  await client.retrieveServerMetadata();
116
149
  }
150
+ if (
151
+ credentialOfferClient.supportedFlows.includes(AuthzFlowType.AUTHORIZATION_CODE_FLOW) &&
152
+ (createAuthorizationRequestURL === undefined || createAuthorizationRequestURL)
153
+ ) {
154
+ console.log(`AUTH REQ`);
155
+ await client.createAuthorizationRequestUrl({ authorizationRequest, pkce });
156
+ console.log(`AUTH REQ URL: ${client._authorizationURL}`);
157
+ }
158
+
117
159
  return client;
118
160
  }
119
161
 
162
+ public async createAuthorizationRequestUrl(opts?: { authorizationRequest?: AuthorizationRequestOpts; pkce?: PKCEOpts }): Promise<string> {
163
+ if (!this._authorizationURL) {
164
+ this.calculatePKCEOpts(opts?.pkce);
165
+ this.syncAuthorizationRequestOpts(opts?.authorizationRequest);
166
+ if (!this._authorizationRequestOpts) {
167
+ throw Error(`No Authorization Request options present or provided in this call`);
168
+ }
169
+
170
+ // todo: Probably can go with current logic in MetadataClient who will always set the authorization_endpoint when found
171
+ // handling this because of the support for v1_0-08
172
+ if (
173
+ this._endpointMetadata &&
174
+ this._endpointMetadata.credentialIssuerMetadata &&
175
+ 'authorization_endpoint' in this._endpointMetadata.credentialIssuerMetadata
176
+ ) {
177
+ this._endpointMetadata.authorization_endpoint = this._endpointMetadata.credentialIssuerMetadata.authorization_endpoint as string;
178
+ }
179
+ this._authorizationURL = await createAuthorizationRequestUrl({
180
+ pkce: this._pkce,
181
+ endpointMetadata: this.endpointMetadata,
182
+ authorizationRequest: this._authorizationRequestOpts,
183
+ credentialOffer: this.credentialOffer,
184
+ credentialsSupported: this.getCredentialsSupported(true),
185
+ });
186
+ }
187
+ return this._authorizationURL;
188
+ }
189
+
120
190
  public async retrieveServerMetadata(): Promise<EndpointMetadataResult> {
121
191
  this.assertIssuerData();
122
192
  if (!this._endpointMetadata) {
@@ -131,117 +201,8 @@ export class OpenID4VCIClient {
131
201
  return this.endpointMetadata;
132
202
  }
133
203
 
134
- public async createAuthorizationRequestUrl(opts: AuthRequestOpts): Promise<string> {
135
- const { redirectUri } = opts;
136
- let { scope, authorizationDetails } = opts;
137
- const parMode = this._endpointMetadata?.credentialIssuerMetadata?.require_pushed_authorization_requests
138
- ? PARMode.REQUIRE
139
- : opts.parMode ?? PARMode.AUTO;
140
- this._pkce = createPKCEOpts({ ...this._pkce, ...opts.pkce });
141
- // Scope and authorization_details can be used in the same authorization request
142
- // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param
143
- if (!scope && !authorizationDetails) {
144
- if (!this.credentialOffer) {
145
- throw Error('Please provide a scope or authorization_details');
146
- }
147
- const creds = this.credentialOffer.credential_offer.credentials;
148
-
149
- authorizationDetails = creds
150
- .flatMap((cred) => (typeof cred === 'string' ? this.getCredentialsSupported(true) : (cred as CredentialSupported)))
151
- .map((cred) => {
152
- return {
153
- ...cred,
154
- type: 'openid_credential',
155
- locations: [this._credentialIssuer],
156
- format: cred.format,
157
- } satisfies AuthDetails;
158
- });
159
- if (authorizationDetails.length === 0) {
160
- throw Error(`Could not create authorization details from credential offer. Please pass in explicit details`);
161
- }
162
- }
163
- // todo: Probably can go with current logic in MetadataClient who will always set the authorization_endpoint when found
164
- // handling this because of the support for v1_0-08
165
- if (
166
- this._endpointMetadata &&
167
- this._endpointMetadata.credentialIssuerMetadata &&
168
- 'authorization_endpoint' in this._endpointMetadata.credentialIssuerMetadata
169
- ) {
170
- this._endpointMetadata.authorization_endpoint = this._endpointMetadata.credentialIssuerMetadata.authorization_endpoint as string;
171
- }
172
- if (!this._endpointMetadata?.authorization_endpoint) {
173
- throw Error('Server metadata does not contain authorization endpoint');
174
- }
175
- const parEndpoint = this._endpointMetadata.credentialIssuerMetadata?.pushed_authorization_request_endpoint;
176
-
177
- // add 'openid' scope if not present
178
- if (!scope?.includes('openid')) {
179
- scope = ['openid', scope].filter((s) => !!s).join(' ');
180
- }
181
-
182
- let queryObj: { [key: string]: string } | PushedAuthorizationResponse = {
183
- response_type: ResponseType.AUTH_CODE,
184
- ...(!this._pkce.disabled && {
185
- code_challenge_method: this._pkce.codeChallengeMethod ?? CodeChallengeMethod.S256,
186
- code_challenge: this._pkce.codeChallenge,
187
- }),
188
- authorization_details: JSON.stringify(this.handleAuthorizationDetails(authorizationDetails)),
189
- redirect_uri: redirectUri,
190
- scope: scope,
191
- };
192
-
193
- if (this.clientId) {
194
- queryObj['client_id'] = this.clientId;
195
- }
196
-
197
- if (this.credentialOffer?.issuerState) {
198
- queryObj['issuer_state'] = this.credentialOffer.issuerState;
199
- }
200
- if (!parEndpoint && parMode === PARMode.REQUIRE) {
201
- throw Error(`PAR mode is set to required by Authorization Server does not support PAR!`);
202
- } else if (parEndpoint && parMode !== PARMode.NEVER) {
203
- const parResponse = await formPost<PushedAuthorizationResponse>(parEndpoint, new URLSearchParams(queryObj));
204
- if (parResponse.errorBody || !parResponse.successBody) {
205
- throw Error(`PAR error`);
206
- }
207
- queryObj = { request_uri: parResponse.successBody.request_uri };
208
- }
209
-
210
- return convertJsonToURI(queryObj, {
211
- baseUrl: this._endpointMetadata.authorization_endpoint,
212
- uriTypeProperties: ['request_uri', 'redirect_uri', 'scope', 'authorization_details', 'issuer_state'],
213
- mode: JsonURIMode.X_FORM_WWW_URLENCODED,
214
- // We do not add the version here, as this always needs to be form encoded
215
- });
216
- }
217
-
218
- public handleAuthorizationDetails(authorizationDetails?: AuthDetails | AuthDetails[]): AuthDetails | AuthDetails[] | undefined {
219
- if (authorizationDetails) {
220
- if (Array.isArray(authorizationDetails)) {
221
- return authorizationDetails.map((value) => this.handleLocations({ ...value }));
222
- } else {
223
- return this.handleLocations({ ...authorizationDetails });
224
- }
225
- }
226
- return authorizationDetails;
227
- }
228
-
229
- private handleLocations(authorizationDetails: AuthDetails) {
230
- if (
231
- authorizationDetails &&
232
- (this.endpointMetadata.credentialIssuerMetadata?.authorization_server || this.endpointMetadata.authorization_endpoint)
233
- ) {
234
- if (authorizationDetails.locations) {
235
- if (Array.isArray(authorizationDetails.locations)) {
236
- (authorizationDetails.locations as string[]).push(this.endpointMetadata.issuer);
237
- } else {
238
- authorizationDetails.locations = [authorizationDetails.locations as string, this.endpointMetadata.issuer];
239
- }
240
- } else {
241
- authorizationDetails.locations = this.endpointMetadata.issuer;
242
- }
243
- }
244
- return authorizationDetails;
204
+ private calculatePKCEOpts(pkce?: PKCEOpts) {
205
+ this._pkce = generateMissingPKCEOpts({ ...this._pkce, ...pkce });
245
206
  }
246
207
 
247
208
  public async acquireAccessToken(opts?: {
@@ -452,7 +413,22 @@ export class OpenID4VCIClient {
452
413
  }
453
414
 
454
415
  issuerSupportedFlowTypes(): AuthzFlowType[] {
455
- return this.credentialOffer?.supportedFlows ?? [AuthzFlowType.AUTHORIZATION_CODE_FLOW];
416
+ return (
417
+ this.credentialOffer?.supportedFlows ??
418
+ (this._endpointMetadata?.credentialIssuerMetadata?.authorization_endpoint ? [AuthzFlowType.AUTHORIZATION_CODE_FLOW] : [])
419
+ );
420
+ }
421
+
422
+ isFlowTypeSupported(flowType: AuthzFlowType): boolean {
423
+ return this.issuerSupportedFlowTypes().includes(flowType);
424
+ }
425
+
426
+ get authorizationURL(): string | undefined {
427
+ return this._authorizationURL;
428
+ }
429
+
430
+ public hasAuthorizationURL(): boolean {
431
+ return !!this.authorizationURL;
456
432
  }
457
433
 
458
434
  get credentialOffer(): CredentialOfferRequestWithBaseUrl | undefined {
@@ -522,10 +498,10 @@ export class OpenID4VCIClient {
522
498
  }
523
499
 
524
500
  private assertIssuerData(): void {
525
- if (!this._credentialOffer && this.issuerSupportedFlowTypes().includes(AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW)) {
526
- throw Error(`No issuance initiation or credential offer present`);
527
- } else if (!this._credentialIssuer) {
501
+ if (!this._credentialIssuer) {
528
502
  throw Error(`No credential issuer value present`);
503
+ } else if (!this._credentialOffer && this._endpointMetadata && this.issuerSupportedFlowTypes().length === 0) {
504
+ throw Error(`No issuance initiation or credential offer present`);
529
505
  }
530
506
  }
531
507
 
@@ -540,4 +516,21 @@ export class OpenID4VCIClient {
540
516
  throw Error(`No access token present`);
541
517
  }
542
518
  }
519
+
520
+ private syncAuthorizationRequestOpts(opts?: AuthorizationRequestOpts) {
521
+ if (!this._authorizationRequestOpts && !opts) {
522
+ return;
523
+ }
524
+ const authorizationRequestOpts = { ...this._authorizationRequestOpts, ...opts } as AuthorizationRequestOpts;
525
+ if (authorizationRequestOpts.clientId) {
526
+ this._clientId = authorizationRequestOpts.clientId;
527
+ }
528
+ if (this._clientId && authorizationRequestOpts) {
529
+ authorizationRequestOpts.clientId = this._clientId;
530
+ }
531
+ if (!authorizationRequestOpts.redirectUri) {
532
+ authorizationRequestOpts.redirectUri = 'openid4vc%3A';
533
+ }
534
+ this._authorizationRequestOpts = authorizationRequestOpts;
535
+ }
543
536
  }
@@ -69,7 +69,9 @@ describe('OID4VCI-Client using Sphereon issuer should', () => {
69
69
 
70
70
  if (credentialType !== 'CTWalletCrossPreAuthorisedInTime') {
71
71
  const url = await client.createAuthorizationRequestUrl({
72
- redirectUri: 'openid4vc%3A',
72
+ authorizationRequest: {
73
+ redirectUri: 'openid4vc%3A',
74
+ },
73
75
  });
74
76
  const result = await fetch(url);
75
77
  console.log(result.text());