@auth0/auth0-server-js 1.2.0 → 1.4.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.
package/dist/index.js CHANGED
@@ -34,6 +34,20 @@ var MissingSessionError = class extends Error {
34
34
  this.name = "MissingSessionError";
35
35
  }
36
36
  };
37
+ var InvalidConfigurationError = class extends Error {
38
+ code = "invalid_configuration_error";
39
+ constructor(message) {
40
+ super(message);
41
+ this.name = "InvalidConfigurationError";
42
+ }
43
+ };
44
+ var IssuerValidationError = class extends Error {
45
+ code = "issuer_validation_error";
46
+ constructor(message) {
47
+ super(message);
48
+ this.name = "IssuerValidationError";
49
+ }
50
+ };
37
51
 
38
52
  // src/state/utils.ts
39
53
  var createUpdatedTokenSet = (audience, response) => ({
@@ -42,19 +56,20 @@ var createUpdatedTokenSet = (audience, response) => ({
42
56
  scope: response.scope,
43
57
  expiresAt: response.expiresAt
44
58
  });
45
- function updateStateData(audience, stateDate, tokenEndpointResponse) {
46
- if (stateDate) {
47
- const isNewTokenSet = !stateDate.tokenSets.some(
59
+ function updateStateData(audience, stateData, tokenEndpointResponse, context) {
60
+ if (stateData) {
61
+ const isNewTokenSet = !stateData.tokenSets.some(
48
62
  (tokenSet) => tokenSet.audience === audience && tokenSet.scope === tokenEndpointResponse.scope
49
63
  );
50
- const tokenSets = isNewTokenSet ? [...stateDate.tokenSets, createUpdatedTokenSet(audience, tokenEndpointResponse)] : stateDate.tokenSets.map(
64
+ const tokenSets = isNewTokenSet ? [...stateData.tokenSets, createUpdatedTokenSet(audience, tokenEndpointResponse)] : stateData.tokenSets.map(
51
65
  (tokenSet) => tokenSet.audience === audience && tokenSet.scope === tokenEndpointResponse.scope ? createUpdatedTokenSet(audience, tokenEndpointResponse) : tokenSet
52
66
  );
53
67
  return {
54
- ...stateDate,
55
- idToken: tokenEndpointResponse.idToken,
56
- refreshToken: tokenEndpointResponse.refreshToken ?? stateDate.refreshToken,
57
- tokenSets
68
+ ...stateData,
69
+ idToken: tokenEndpointResponse.idToken ?? stateData.idToken,
70
+ refreshToken: tokenEndpointResponse.refreshToken ?? stateData.refreshToken,
71
+ tokenSets,
72
+ domain: context?.domain ?? stateData.domain
58
73
  };
59
74
  } else {
60
75
  const user = tokenEndpointResponse.claims;
@@ -62,9 +77,8 @@ function updateStateData(audience, stateDate, tokenEndpointResponse) {
62
77
  user,
63
78
  idToken: tokenEndpointResponse.idToken,
64
79
  refreshToken: tokenEndpointResponse.refreshToken,
65
- tokenSets: [
66
- createUpdatedTokenSet(audience, tokenEndpointResponse)
67
- ],
80
+ tokenSets: [createUpdatedTokenSet(audience, tokenEndpointResponse)],
81
+ domain: context?.domain,
68
82
  internal: {
69
83
  sid: user?.sid,
70
84
  createdAt: Math.floor(Date.now() / 1e3)
@@ -72,9 +86,9 @@ function updateStateData(audience, stateDate, tokenEndpointResponse) {
72
86
  };
73
87
  }
74
88
  }
75
- function updateStateDataForConnectionTokenSet(options, stateDate, tokenEndpointResponse) {
76
- stateDate.connectionTokenSets = stateDate.connectionTokenSets || [];
77
- const isNewTokenSet = !stateDate.connectionTokenSets.some(
89
+ function updateStateDataForConnectionTokenSet(options, stateData, tokenEndpointResponse) {
90
+ stateData.connectionTokenSets = stateData.connectionTokenSets || [];
91
+ const isNewTokenSet = !stateData.connectionTokenSets.some(
78
92
  (tokenSet) => tokenSet.connection === options.connection && (!options.loginHint || tokenSet.loginHint === options.loginHint)
79
93
  );
80
94
  const connectionTokenSet = {
@@ -84,11 +98,11 @@ function updateStateDataForConnectionTokenSet(options, stateDate, tokenEndpointR
84
98
  scope: tokenEndpointResponse.scope,
85
99
  expiresAt: tokenEndpointResponse.expiresAt
86
100
  };
87
- const connectionTokenSets = isNewTokenSet ? [...stateDate.connectionTokenSets, connectionTokenSet] : stateDate.connectionTokenSets.map(
101
+ const connectionTokenSets = isNewTokenSet ? [...stateData.connectionTokenSets, connectionTokenSet] : stateData.connectionTokenSets.map(
88
102
  (tokenSet) => tokenSet.connection === options.connection && (!options.loginHint || tokenSet.loginHint === options.loginHint) ? connectionTokenSet : tokenSet
89
103
  );
90
104
  return {
91
- ...stateDate,
105
+ ...stateData,
92
106
  connectionTokenSets
93
107
  };
94
108
  }
@@ -114,19 +128,70 @@ var compareScopes = (scopes, requiredScopes) => {
114
128
  };
115
129
 
116
130
  // src/server-client.ts
131
+ import { decodeJwt } from "jose";
132
+
133
+ // src/telemetry.ts
134
+ function getTelemetryConfig(config) {
135
+ if (config?.enabled === false) {
136
+ return config;
137
+ }
138
+ return {
139
+ enabled: true,
140
+ name: config?.name ?? "@auth0/auth0-server-js",
141
+ version: config?.version ?? "1.4.0"
142
+ };
143
+ }
144
+
145
+ // src/server-client.ts
146
+ var DEFAULT_SCOPES = "openid profile email offline_access";
147
+ var normalizeDomain = (value) => {
148
+ const trimmed = value.trim();
149
+ const parsed = trimmed.startsWith("http") ? new URL(trimmed) : new URL(`https://${trimmed}`);
150
+ return parsed.host.toLowerCase();
151
+ };
152
+ var decodeIssuer = (token) => {
153
+ try {
154
+ const { iss } = decodeJwt(token);
155
+ return typeof iss === "string" ? iss : void 0;
156
+ } catch {
157
+ return void 0;
158
+ }
159
+ };
160
+ var ensureOpenIdScope = (scope) => {
161
+ if (!scope) {
162
+ return DEFAULT_SCOPES;
163
+ }
164
+ const scopes = scope.split(" ");
165
+ if (!scopes.includes("openid")) {
166
+ scopes.unshift("openid");
167
+ }
168
+ return scopes.join(" ");
169
+ };
117
170
  var ServerClient = class {
118
171
  #options;
119
172
  #transactionStore;
120
173
  #transactionStoreIdentifier;
121
174
  #stateStore;
122
175
  #stateStoreIdentifier;
176
+ #authClientOptions;
177
+ #staticDomain;
178
+ #authClient;
123
179
  /**
124
180
  * The underlying `authClient` instance that can be used to interact with the Auth0 Authentication API.
125
181
  * Generally, you should prefer to use the higher-level methods exposed on the `ServerClient` instance.
126
- *
182
+ *
183
+ * This property can only be used when `domain` is configured as a static string.
184
+ * In resolver mode (`domain` as a function), the SDK resolves the domain per request,
185
+ * so use `ServerClient` methods instead.
186
+ *
127
187
  * Important: the methods exposed on the `authClient` instance do not handle any session or state management.
128
188
  */
129
- authClient;
189
+ get authClient() {
190
+ if (!this.#authClient) {
191
+ throw new InvalidConfigurationError("authClient is only available when using a static domain configuration.");
192
+ }
193
+ return this.#authClient;
194
+ }
130
195
  constructor(options) {
131
196
  this.#options = options;
132
197
  this.#stateStoreIdentifier = this.#options.stateIdentifier || "__a0_session";
@@ -139,17 +204,77 @@ var ServerClient = class {
139
204
  if (!this.#options.transactionStore) {
140
205
  throw new MissingRequiredArgumentError("transactionStore");
141
206
  }
142
- this.authClient = new AuthClient({
143
- domain: this.#options.domain,
207
+ if (typeof this.#options.domain !== "string" && typeof this.#options.domain !== "function") {
208
+ throw new InvalidConfigurationError("domain must be a string or resolver function");
209
+ }
210
+ this.#authClientOptions = {
144
211
  clientId: this.#options.clientId,
145
212
  clientSecret: this.#options.clientSecret,
146
213
  clientAssertionSigningKey: this.#options.clientAssertionSigningKey,
147
214
  clientAssertionSigningAlg: this.#options.clientAssertionSigningAlg,
148
215
  authorizationParams: this.#options.authorizationParams,
216
+ discoveryCache: this.#options.discoveryCache,
149
217
  customFetch: this.#options.customFetch,
150
218
  useMtls: this.#options.useMtls
219
+ };
220
+ if (typeof this.#options.domain === "string") {
221
+ const domain = normalizeDomain(this.#options.domain);
222
+ this.#staticDomain = domain;
223
+ this.#authClient = new AuthClient({
224
+ domain,
225
+ ...this.#authClientOptions,
226
+ telemetry: getTelemetryConfig(this.#options.telemetry)
227
+ });
228
+ }
229
+ }
230
+ async #resolveDomain(storeOptions) {
231
+ if (typeof this.#options.domain === "function") {
232
+ const resolved = await this.#options.domain(storeOptions);
233
+ if (typeof resolved !== "string" || resolved.trim().length === 0) {
234
+ throw new InvalidConfigurationError("domainResolver returned no domain");
235
+ }
236
+ return normalizeDomain(resolved);
237
+ }
238
+ return normalizeDomain(this.#options.domain);
239
+ }
240
+ #createAuthClient(domain) {
241
+ return new AuthClient({
242
+ domain,
243
+ ...this.#authClientOptions,
244
+ telemetry: getTelemetryConfig(this.#options.telemetry)
151
245
  });
152
246
  }
247
+ #getAuthClient(domain) {
248
+ const normalizedDomain = normalizeDomain(domain);
249
+ if (this.#authClient && this.#staticDomain === normalizedDomain) {
250
+ return this.#authClient;
251
+ }
252
+ return this.#createAuthClient(normalizedDomain);
253
+ }
254
+ #getSessionDomain(stateData) {
255
+ if (stateData.domain) {
256
+ return normalizeDomain(stateData.domain);
257
+ }
258
+ if (this.#staticDomain) {
259
+ return this.#staticDomain;
260
+ }
261
+ const issuerFromClaims = stateData.user?.iss;
262
+ if (typeof issuerFromClaims === "string" && issuerFromClaims.trim().length > 0) {
263
+ return normalizeDomain(issuerFromClaims);
264
+ }
265
+ return;
266
+ }
267
+ #isResolverMode() {
268
+ return typeof this.#options.domain === "function";
269
+ }
270
+ async #isSessionForCurrentDomain(stateData, storeOptions) {
271
+ const sessionDomain = this.#getSessionDomain(stateData);
272
+ if (!sessionDomain) {
273
+ return false;
274
+ }
275
+ const resolvedDomain = await this.#resolveDomain(storeOptions);
276
+ return sessionDomain === resolvedDomain;
277
+ }
153
278
  /**
154
279
  * Starts the interactive login process, and returns a URL to redirect the user-agent to to request authorization at Auth0.
155
280
  * @param options Optional options used to configure the interactive login process.
@@ -164,16 +289,21 @@ var ServerClient = class {
164
289
  if (!redirectUri) {
165
290
  throw new MissingRequiredArgumentError("authorizationParams.redirect_uri");
166
291
  }
167
- const { codeVerifier, authorizationUrl } = await this.authClient.buildAuthorizationUrl({
292
+ const scope = ensureOpenIdScope(options?.authorizationParams?.scope ?? this.#options.authorizationParams?.scope);
293
+ const domain = await this.#resolveDomain(storeOptions);
294
+ const authClient = this.#getAuthClient(domain);
295
+ const { codeVerifier, authorizationUrl } = await authClient.buildAuthorizationUrl({
168
296
  pushedAuthorizationRequests: options?.pushedAuthorizationRequests,
169
297
  authorizationParams: {
170
298
  ...options?.authorizationParams,
171
- redirect_uri: redirectUri
299
+ redirect_uri: redirectUri,
300
+ scope
172
301
  }
173
302
  });
174
303
  const transactionState = {
175
304
  audience: options?.authorizationParams?.audience ?? this.#options.authorizationParams?.audience,
176
- codeVerifier
305
+ codeVerifier,
306
+ domain
177
307
  };
178
308
  if (options?.appState) {
179
309
  transactionState.appState = options.appState;
@@ -197,11 +327,15 @@ var ServerClient = class {
197
327
  if (!transactionData) {
198
328
  throw new MissingTransactionError();
199
329
  }
200
- const tokenEndpointResponse = await this.authClient.getTokenByCode(url, {
330
+ const domain = transactionData.domain ?? await this.#resolveDomain(storeOptions);
331
+ const authClient = this.#getAuthClient(domain);
332
+ const tokenEndpointResponse = await authClient.getTokenByCode(url, {
201
333
  codeVerifier: transactionData.codeVerifier
202
334
  });
203
335
  const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
204
- const stateData = updateStateData(transactionData.audience ?? "default", existingStateData, tokenEndpointResponse);
336
+ const stateData = updateStateData(transactionData.audience ?? "default", existingStateData, tokenEndpointResponse, {
337
+ domain
338
+ });
205
339
  await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
206
340
  await this.#transactionStore.delete(this.#transactionStoreIdentifier, storeOptions);
207
341
  return { appState: transactionData.appState, authorizationDetails: tokenEndpointResponse.authorizationDetails };
@@ -223,7 +357,15 @@ var ServerClient = class {
223
357
  "Unable to start the user linking process without a logged in user. Ensure to login using the SDK before starting the user linking process."
224
358
  );
225
359
  }
226
- const { linkUserUrl, codeVerifier } = await this.authClient.buildLinkUserUrl({
360
+ if (this.#isResolverMode()) {
361
+ const isCurrentDomain = await this.#isSessionForCurrentDomain(stateData, storeOptions);
362
+ if (!isCurrentDomain) {
363
+ throw new MissingSessionError("Session domain does not match the current domain.");
364
+ }
365
+ }
366
+ const domain = this.#getSessionDomain(stateData);
367
+ const authClient = this.#getAuthClient(domain);
368
+ const { linkUserUrl, codeVerifier } = await authClient.buildLinkUserUrl({
227
369
  connection: options.connection,
228
370
  connectionScope: options.connectionScope,
229
371
  idToken: stateData.idToken,
@@ -231,7 +373,8 @@ var ServerClient = class {
231
373
  });
232
374
  const transactionState = {
233
375
  audience: options?.authorizationParams?.audience ?? this.#options.authorizationParams?.audience,
234
- codeVerifier
376
+ codeVerifier,
377
+ domain
235
378
  };
236
379
  if (options?.appState) {
237
380
  transactionState.appState = options.appState;
@@ -273,14 +416,23 @@ var ServerClient = class {
273
416
  "Unable to start the user unlinking process without a logged in user. Ensure to login using the SDK before starting the user unlinking process."
274
417
  );
275
418
  }
276
- const { unlinkUserUrl, codeVerifier } = await this.authClient.buildUnlinkUserUrl({
419
+ if (this.#isResolverMode()) {
420
+ const isCurrentDomain = await this.#isSessionForCurrentDomain(stateData, storeOptions);
421
+ if (!isCurrentDomain) {
422
+ throw new MissingSessionError("Session domain does not match the current domain.");
423
+ }
424
+ }
425
+ const domain = this.#getSessionDomain(stateData);
426
+ const authClient = this.#getAuthClient(domain);
427
+ const { unlinkUserUrl, codeVerifier } = await authClient.buildUnlinkUserUrl({
277
428
  connection: options.connection,
278
429
  idToken: stateData.idToken,
279
430
  authorizationParams: options.authorizationParams
280
431
  });
281
432
  const transactionState = {
282
433
  audience: options?.authorizationParams?.audience ?? this.#options.authorizationParams?.audience,
283
- codeVerifier
434
+ codeVerifier,
435
+ domain
284
436
  };
285
437
  if (options?.appState) {
286
438
  transactionState.appState = options.appState;
@@ -318,16 +470,23 @@ var ServerClient = class {
318
470
  * @returns A promise resolving to an object, containing the authorizationDetails (when RAR was used).
319
471
  */
320
472
  async loginBackchannel(options, storeOptions) {
321
- const tokenEndpointResponse = await this.authClient.backchannelAuthentication({
473
+ const scope = ensureOpenIdScope(options.authorizationParams?.scope ?? this.#options.authorizationParams?.scope);
474
+ const domain = await this.#resolveDomain(storeOptions);
475
+ const authClient = this.#getAuthClient(domain);
476
+ const tokenEndpointResponse = await authClient.backchannelAuthentication({
322
477
  bindingMessage: options.bindingMessage,
323
478
  loginHint: options.loginHint,
324
- authorizationParams: options.authorizationParams
479
+ authorizationParams: {
480
+ ...options.authorizationParams,
481
+ scope
482
+ }
325
483
  });
326
484
  const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
327
485
  const stateData = updateStateData(
328
486
  this.#options.authorizationParams?.audience ?? "default",
329
487
  existingStateData,
330
- tokenEndpointResponse
488
+ tokenEndpointResponse,
489
+ { domain }
331
490
  );
332
491
  await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
333
492
  return {
@@ -341,16 +500,31 @@ var ServerClient = class {
341
500
  */
342
501
  async getUser(storeOptions) {
343
502
  const stateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
344
- return stateData?.user;
503
+ if (!stateData) {
504
+ return;
505
+ }
506
+ if (this.#isResolverMode()) {
507
+ const isCurrentDomain = await this.#isSessionForCurrentDomain(stateData, storeOptions);
508
+ if (!isCurrentDomain) {
509
+ return;
510
+ }
511
+ }
512
+ return stateData.user;
345
513
  }
346
514
  /**
347
515
  * Retrieve the user session from the store, or undefined if no session found.
348
516
  * @param storeOptions Optional options used to pass to the Transaction and State Store.
349
- * @returns The sessionm or undefined if no session found in the store.
517
+ * @returns The session or undefined if no session found in the store.
350
518
  */
351
519
  async getSession(storeOptions) {
352
520
  const stateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
353
521
  if (stateData) {
522
+ if (this.#isResolverMode()) {
523
+ const isCurrentDomain = await this.#isSessionForCurrentDomain(stateData, storeOptions);
524
+ if (!isCurrentDomain) {
525
+ return;
526
+ }
527
+ }
354
528
  const { internal, ...sessionData } = stateData;
355
529
  return sessionData;
356
530
  }
@@ -368,6 +542,19 @@ var ServerClient = class {
368
542
  const stateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
369
543
  const audience = this.#options.authorizationParams?.audience ?? "default";
370
544
  const scope = this.#options.authorizationParams?.scope;
545
+ const sessionDomain = stateData ? this.#getSessionDomain(stateData) : this.#staticDomain;
546
+ if (this.#isResolverMode()) {
547
+ if (!stateData) {
548
+ throw new MissingSessionError("Unable to retrieve access token without a logged in user.");
549
+ }
550
+ if (!sessionDomain) {
551
+ throw new MissingSessionError("Session domain does not match the current domain.");
552
+ }
553
+ const resolvedDomain = await this.#resolveDomain(storeOptions);
554
+ if (sessionDomain !== resolvedDomain) {
555
+ throw new MissingSessionError("Session domain does not match the current domain.");
556
+ }
557
+ }
371
558
  const tokenSet = stateData?.tokenSets.find(
372
559
  (tokenSet2) => tokenSet2.audience === audience && (!scope || compareScopes(tokenSet2.scope, scope))
373
560
  );
@@ -379,11 +566,14 @@ var ServerClient = class {
379
566
  "The access token has expired and a refresh token was not provided. The user needs to re-authenticate."
380
567
  );
381
568
  }
382
- const tokenEndpointResponse = await this.authClient.getTokenByRefreshToken({
569
+ const domainForSession = sessionDomain;
570
+ const tokenEndpointResponse = await this.#getAuthClient(domainForSession).getTokenByRefreshToken({
383
571
  refreshToken: stateData.refreshToken
384
572
  });
385
573
  const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
386
- const updatedStateData = updateStateData(audience, existingStateData, tokenEndpointResponse);
574
+ const updatedStateData = updateStateData(audience, existingStateData, tokenEndpointResponse, {
575
+ domain: domainForSession
576
+ });
387
577
  await this.#stateStore.set(this.#stateStoreIdentifier, updatedStateData, false, storeOptions);
388
578
  return {
389
579
  accessToken: tokenEndpointResponse.accessToken,
@@ -409,6 +599,19 @@ var ServerClient = class {
409
599
  */
410
600
  async getAccessTokenForConnection(options, storeOptions) {
411
601
  const stateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
602
+ const sessionDomain = stateData ? this.#getSessionDomain(stateData) : this.#staticDomain;
603
+ if (this.#isResolverMode()) {
604
+ if (!stateData) {
605
+ throw new MissingSessionError("Unable to retrieve an access token for a connection without a logged in user.");
606
+ }
607
+ if (!sessionDomain) {
608
+ throw new MissingSessionError("Session domain does not match the current domain.");
609
+ }
610
+ const resolvedDomain = await this.#resolveDomain(storeOptions);
611
+ if (sessionDomain !== resolvedDomain) {
612
+ throw new MissingSessionError("Session domain does not match the current domain.");
613
+ }
614
+ }
412
615
  const connectionTokenSet = stateData?.connectionTokenSets?.find(
413
616
  (tokenSet) => tokenSet.connection === options.connection
414
617
  );
@@ -420,12 +623,20 @@ var ServerClient = class {
420
623
  "A refresh token was not found but is required to be able to retrieve an access token for a connection."
421
624
  );
422
625
  }
423
- const tokenEndpointResponse = await this.authClient.getTokenForConnection({
626
+ const domainForSession = sessionDomain;
627
+ const tokenEndpointResponse = await this.#getAuthClient(domainForSession).getTokenForConnection({
424
628
  connection: options.connection,
425
629
  loginHint: options.loginHint,
426
630
  refreshToken: stateData.refreshToken
427
631
  });
428
- const updatedStateData = updateStateDataForConnectionTokenSet(options, stateData, tokenEndpointResponse);
632
+ const updatedStateData = updateStateDataForConnectionTokenSet(
633
+ options,
634
+ {
635
+ ...stateData,
636
+ domain: stateData.domain ?? domainForSession
637
+ },
638
+ tokenEndpointResponse
639
+ );
429
640
  await this.#stateStore.set(this.#stateStoreIdentifier, updatedStateData, false, storeOptions);
430
641
  return {
431
642
  accessToken: tokenEndpointResponse.accessToken,
@@ -442,8 +653,21 @@ var ServerClient = class {
442
653
  * @returns {URL}
443
654
  */
444
655
  async logout(options, storeOptions) {
445
- await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
446
- return this.authClient.buildLogoutUrl(options);
656
+ if (!this.#isResolverMode()) {
657
+ await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
658
+ return this.authClient.buildLogoutUrl(options);
659
+ }
660
+ const resolvedDomain = await this.#resolveDomain(storeOptions);
661
+ const authClient = this.#getAuthClient(resolvedDomain);
662
+ const stateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
663
+ const sessionDomain = stateData ? this.#getSessionDomain(stateData) : void 0;
664
+ if (!stateData) {
665
+ return authClient.buildLogoutUrl(options);
666
+ }
667
+ if (sessionDomain && sessionDomain === resolvedDomain) {
668
+ await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
669
+ }
670
+ return authClient.buildLogoutUrl(options);
447
671
  }
448
672
  /**
449
673
  * Handles the backchannel logout process by verifying the logout token and deleting the session from the store if the logout token was considered valid.
@@ -457,19 +681,37 @@ var ServerClient = class {
457
681
  if (!logoutToken) {
458
682
  throw new BackchannelLogoutError("Missing Logout Token");
459
683
  }
460
- const logoutTokenClaims = await this.authClient.verifyLogoutToken({ logoutToken });
461
- await this.#stateStore.deleteByLogoutToken(logoutTokenClaims, storeOptions);
684
+ if (!this.#isResolverMode()) {
685
+ const logoutTokenClaims2 = await this.authClient.verifyLogoutToken({ logoutToken });
686
+ await this.#stateStore.deleteByLogoutToken(logoutTokenClaims2, storeOptions);
687
+ return;
688
+ }
689
+ const issuer = decodeIssuer(logoutToken);
690
+ if (!issuer) {
691
+ throw new BackchannelLogoutError("Logout token is missing an issuer");
692
+ }
693
+ const resolvedDomain = await this.#resolveDomain(storeOptions);
694
+ const domain = normalizeDomain(issuer);
695
+ if (domain !== resolvedDomain) {
696
+ throw new BackchannelLogoutError("Logout token issuer does not match the resolved domain");
697
+ }
698
+ const authClient = this.#getAuthClient(domain);
699
+ const logoutTokenClaims = await authClient.verifyLogoutToken({ logoutToken });
700
+ await this.#stateStore.deleteByLogoutToken({ ...logoutTokenClaims, iss: issuer }, storeOptions);
462
701
  }
463
702
  };
464
703
 
465
704
  // src/encryption/index.ts
466
- import { EncryptJWT, jwtDecrypt } from "jose";
705
+ import { EncryptJWT, jwtDecrypt, errors } from "jose";
467
706
  var ENC = "A256CBC-HS512";
468
707
  var ALG = "dir";
469
708
  var DIGEST = "SHA-256";
470
709
  var BIT_LENGTH = 512;
471
710
  var HKDF_INFO = "derived cookie encryption secret";
472
711
  var encoder;
712
+ function isDecryptionError(e) {
713
+ return e instanceof errors.JWEDecryptionFailed || e instanceof errors.JWEInvalid;
714
+ }
473
715
  async function deriveEncryptionSecret(secret, salt, kid) {
474
716
  encoder ||= new TextEncoder();
475
717
  const key = await crypto.subtle.importKey("raw", encoder.encode(secret), "HKDF", false, ["deriveBits"]);
@@ -486,12 +728,23 @@ async function deriveEncryptionSecret(secret, salt, kid) {
486
728
  )
487
729
  );
488
730
  }
489
- async function encrypt(payload, secret, salt, expiration) {
731
+ async function _encrypt(payload, secret, salt, expiration) {
490
732
  const kid = crypto.randomUUID();
491
733
  const encryptionSecret = await deriveEncryptionSecret(secret, salt, kid);
492
734
  return await new EncryptJWT(payload).setProtectedHeader({ enc: ENC, alg: ALG, kid }).setExpirationTime(expiration).encrypt(encryptionSecret);
493
735
  }
494
- async function decrypt(value, secret, salt) {
736
+ async function encrypt(payload, secret, salt, expiration) {
737
+ if (typeof secret === "string") {
738
+ return await _encrypt(payload, secret, salt, expiration);
739
+ } else {
740
+ const [newSecret] = secret;
741
+ if (!newSecret) {
742
+ throw new InvalidConfigurationError("At least one secret must be provided");
743
+ }
744
+ return await _encrypt(payload, newSecret, salt, expiration);
745
+ }
746
+ }
747
+ async function _decrypt(value, secret, salt) {
495
748
  const res = await jwtDecrypt(
496
749
  value,
497
750
  async (protectedHeader) => {
@@ -504,6 +757,35 @@ async function decrypt(value, secret, salt) {
504
757
  );
505
758
  return res.payload;
506
759
  }
760
+ async function decrypt(value, secret, salt) {
761
+ if (typeof secret === "string") {
762
+ return await _decrypt(value, secret, salt);
763
+ } else {
764
+ const [newSecret, ...oldSecrets] = secret;
765
+ if (!newSecret) {
766
+ throw new InvalidConfigurationError("At least one secret must be provided");
767
+ }
768
+ let firstError;
769
+ try {
770
+ return await _decrypt(value, newSecret, salt);
771
+ } catch (err) {
772
+ firstError = err;
773
+ if (!isDecryptionError(err)) {
774
+ throw firstError;
775
+ }
776
+ for (const oldSecret of oldSecrets) {
777
+ try {
778
+ return await _decrypt(value, oldSecret, salt);
779
+ } catch (fallbackErr) {
780
+ if (!isDecryptionError(fallbackErr)) {
781
+ throw fallbackErr;
782
+ }
783
+ }
784
+ }
785
+ throw firstError;
786
+ }
787
+ }
788
+ }
507
789
 
508
790
  // src/store/abstract-store.ts
509
791
  var AbstractStore = class {
@@ -515,7 +797,14 @@ var AbstractStore = class {
515
797
  return await encrypt(stateData, this.options.secret, identifier, expiration);
516
798
  }
517
799
  async decrypt(identifier, encryptedStateData) {
518
- return await decrypt(encryptedStateData, this.options.secret, identifier);
800
+ try {
801
+ return await decrypt(encryptedStateData, this.options.secret, identifier);
802
+ } catch (e) {
803
+ if (isDecryptionError(e)) {
804
+ return;
805
+ }
806
+ throw e;
807
+ }
519
808
  }
520
809
  };
521
810
 
@@ -642,7 +931,9 @@ var StatefulStateStore = class extends AbstractSessionStore {
642
931
  const cookieValue = this.#cookieHandler.getCookie(identifier, options);
643
932
  if (cookieValue) {
644
933
  const sessionCookie = await this.decrypt(identifier, cookieValue);
645
- return sessionCookie.id;
934
+ if (sessionCookie) {
935
+ return sessionCookie.id;
936
+ }
646
937
  }
647
938
  }
648
939
  deleteByLogoutToken(claims, options) {
@@ -729,6 +1020,8 @@ export {
729
1020
  AbstractTransactionStore,
730
1021
  BackchannelLogoutError,
731
1022
  CookieTransactionStore,
1023
+ InvalidConfigurationError,
1024
+ IssuerValidationError,
732
1025
  MissingRequiredArgumentError,
733
1026
  MissingSessionError,
734
1027
  MissingTransactionError,