@auth0/auth0-server-js 1.6.1 → 1.8.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
@@ -48,14 +48,55 @@ var IssuerValidationError = class extends Error {
48
48
  this.name = "IssuerValidationError";
49
49
  }
50
50
  };
51
+ var SessionExpiredError = class extends Error {
52
+ code = "session_expired";
53
+ constructor(message) {
54
+ super(
55
+ message ?? "The session has expired because the upstream identity provider session ceiling was reached. The user needs to re-authenticate."
56
+ );
57
+ this.name = "SessionExpiredError";
58
+ }
59
+ };
51
60
 
52
61
  // src/state/utils.ts
62
+ var SESSION_EXPIRY_LEEWAY = 30;
63
+ var MAX_PLAUSIBLE_UNIX_SECONDS = 1e10;
64
+ function isPlausibleUnixSeconds(value) {
65
+ return typeof value === "number" && Number.isInteger(value) && value > 0 && value < MAX_PLAUSIBLE_UNIX_SECONDS;
66
+ }
67
+ function extractSessionExpiry(claims) {
68
+ const value = claims?.session_expiry;
69
+ return isPlausibleUnixSeconds(value) ? value : void 0;
70
+ }
71
+ function isSessionExpiryReached(sessionExpiresAt, nowSeconds) {
72
+ if (sessionExpiresAt === void 0) {
73
+ return false;
74
+ }
75
+ const now = nowSeconds ?? Math.floor(Date.now() / 1e3);
76
+ return now >= sessionExpiresAt - SESSION_EXPIRY_LEEWAY;
77
+ }
53
78
  var createUpdatedTokenSet = (audience, response) => ({
54
79
  audience,
55
80
  accessToken: response.accessToken,
56
81
  scope: response.scope,
57
82
  expiresAt: response.expiresAt
58
83
  });
84
+ function isSessionExpiryInPast(sessionExpiresAt, issuedAt) {
85
+ if (sessionExpiresAt === void 0) {
86
+ return false;
87
+ }
88
+ const reference = isPlausibleUnixSeconds(issuedAt) ? issuedAt : Math.floor(Date.now() / 1e3);
89
+ return sessionExpiresAt <= reference + SESSION_EXPIRY_LEEWAY;
90
+ }
91
+ function applySessionExpiryAtLogin(stateData, claims) {
92
+ const sessionExpiresAt = extractSessionExpiry(claims);
93
+ if (isSessionExpiryInPast(sessionExpiresAt, claims?.iat)) {
94
+ throw new SessionExpiredError(
95
+ "The upstream identity provider session_expiry is at or before the issued-at time; refusing to create an already-expired session."
96
+ );
97
+ }
98
+ return { ...stateData, sessionExpiresAt };
99
+ }
59
100
  function updateStateData(audience, stateData, tokenEndpointResponse, context) {
60
101
  if (stateData && tokenEndpointResponse.claims) {
61
102
  const newSub = tokenEndpointResponse.claims.sub;
@@ -122,10 +163,24 @@ function updateStateDataForConnectionTokenSet(options, stateData, tokenEndpointR
122
163
  import {
123
164
  TokenForConnectionError,
124
165
  AuthClient,
166
+ PasswordlessStartError,
167
+ PasswordlessVerifyError,
125
168
  TokenByRefreshTokenError
126
169
  } from "@auth0/auth0-auth-js";
127
170
 
128
171
  // src/utils.ts
172
+ var DEFAULT_SCOPES = "openid profile email offline_access";
173
+ var ensureOpenIdScope = (scope) => {
174
+ const normalizedScope = scope?.trim();
175
+ if (!normalizedScope) {
176
+ return DEFAULT_SCOPES;
177
+ }
178
+ const scopes = normalizedScope.split(/\s+/);
179
+ if (!scopes.includes("openid")) {
180
+ scopes.unshift("openid");
181
+ }
182
+ return scopes.join(" ");
183
+ };
129
184
  var compareScopes = (scopes, requiredScopes) => {
130
185
  if (scopes === requiredScopes) {
131
186
  return true;
@@ -149,7 +204,7 @@ function getTelemetryConfig(config) {
149
204
  return {
150
205
  enabled: true,
151
206
  name: config?.name ?? "@auth0/auth0-server-js",
152
- version: config?.version ?? "1.6.1"
207
+ version: config?.version ?? "1.8.0"
153
208
  };
154
209
  }
155
210
 
@@ -211,9 +266,12 @@ var ServerMfaClient = class {
211
266
  this.#options.stateStoreIdentifier,
212
267
  storeOptions
213
268
  );
214
- const updatedStateData = updateStateData(audience, existingStateData, tokenResponse, {
215
- domain: this.#options.domain
216
- });
269
+ const updatedStateData = applySessionExpiryAtLogin(
270
+ updateStateData(audience, existingStateData, tokenResponse, {
271
+ domain: this.#options.domain
272
+ }),
273
+ tokenResponse.claims
274
+ );
217
275
  await this.#options.stateStore.set(
218
276
  this.#options.stateStoreIdentifier,
219
277
  updatedStateData,
@@ -233,8 +291,99 @@ var ServerMfaClient = class {
233
291
  }
234
292
  };
235
293
 
294
+ // src/passkey/server-passkey-client.ts
295
+ var ServerPasskeyClient = class {
296
+ #options;
297
+ /**
298
+ * @internal
299
+ */
300
+ constructor(options) {
301
+ this.#options = options;
302
+ }
303
+ /**
304
+ * Requests a passkey signup challenge for a new user.
305
+ *
306
+ * Returns the `authSession` and the WebAuthn credential creation options
307
+ * (`authnParamsPublicKey`). The application must return these to the browser,
308
+ * pass `authnParamsPublicKey` to `navigator.credentials.create()`, and then
309
+ * call `getToken()` with the resulting credential to complete signup.
310
+ *
311
+ * This method does not create a session; no state is persisted.
312
+ *
313
+ * @param options User profile data and optional realm/organization.
314
+ * @param storeOptions Optional options used to resolve the domain (resolver mode).
315
+ *
316
+ * @throws {PasskeyRegisterError} If there was an issue requesting the signup challenge.
317
+ *
318
+ * @returns A promise resolving to the signup challenge.
319
+ */
320
+ async register(options, storeOptions) {
321
+ const domain = await this.#options.resolveDomain(storeOptions);
322
+ const authClient = this.#options.getAuthClient(domain);
323
+ return authClient.passkey.register(options);
324
+ }
325
+ /**
326
+ * Requests a passkey login challenge for an existing user.
327
+ *
328
+ * Returns the `authSession` and the WebAuthn credential request options
329
+ * (`authnParamsPublicKey`). The application must return these to the browser,
330
+ * pass `authnParamsPublicKey` to `navigator.credentials.get()`, and then
331
+ * call `getToken()` with the resulting credential to complete login.
332
+ *
333
+ * This method does not create a session; no state is persisted.
334
+ *
335
+ * @param options Optional realm/organization configuration.
336
+ * @param storeOptions Optional options used to resolve the domain (resolver mode).
337
+ *
338
+ * @throws {PasskeyChallengeError} If there was an issue requesting the login challenge.
339
+ *
340
+ * @returns A promise resolving to the login challenge.
341
+ */
342
+ async challenge(options, storeOptions) {
343
+ const domain = await this.#options.resolveDomain(storeOptions);
344
+ const authClient = this.#options.getAuthClient(domain);
345
+ return authClient.passkey.challenge(options);
346
+ }
347
+ /**
348
+ * Completes a passkey authentication flow (signup or login) by exchanging the
349
+ * WebAuthn credential for tokens, and persists the resulting session.
350
+ *
351
+ * Call this after obtaining a credential from `navigator.credentials.create()`
352
+ * (signup) or `navigator.credentials.get()` (login), passing the `authSession`
353
+ * returned by `register()` / `challenge()` together with the serialized credential.
354
+ *
355
+ * In resolver (multi-tenant) mode, pass the same `storeOptions` you passed to
356
+ * `register()` / `challenge()` so the token exchange resolves the same tenant
357
+ * that issued the `authSession`; otherwise the exchange will fail.
358
+ *
359
+ * @param options The auth session, serialized credential, and optional realm/scope/audience/organization.
360
+ * @param storeOptions Optional options used to pass to the State Store (and to resolve the domain in resolver mode).
361
+ *
362
+ * @throws {PasskeyGetTokenError} If there was an issue exchanging the credential for tokens. When the cause is `mfa_required`, use `isMfaRequiredError(error)` to narrow the error and read `cause.mfa_token`. No session is persisted in this case.
363
+ * @throws {OrganizationValidationError} When `organization` is passed and the returned ID token's organization claim is missing or does not match. The error is thrown before the session is written, so no session is persisted in this case.
364
+ *
365
+ * @returns A promise resolving to an object containing the authorizationDetails (when RAR was used).
366
+ */
367
+ async getToken(options, storeOptions) {
368
+ const scope = ensureOpenIdScope(options.scope ?? this.#options.defaultScope);
369
+ const audience = options.audience ?? this.#options.defaultAudience;
370
+ const domain = await this.#options.resolveDomain(storeOptions);
371
+ const authClient = this.#options.getAuthClient(domain);
372
+ const tokenEndpointResponse = await authClient.passkey.getTokenByPasskey({
373
+ ...options,
374
+ scope,
375
+ audience
376
+ });
377
+ const existingStateData = await this.#options.stateStore.get(this.#options.stateStoreIdentifier, storeOptions);
378
+ const stateData = updateStateData(audience ?? "default", existingStateData, tokenEndpointResponse, { domain });
379
+ await this.#options.stateStore.set(this.#options.stateStoreIdentifier, stateData, true, storeOptions);
380
+ return {
381
+ authorizationDetails: tokenEndpointResponse.authorizationDetails
382
+ };
383
+ }
384
+ };
385
+
236
386
  // src/server-client.ts
237
- var DEFAULT_SCOPES = "openid profile email offline_access";
238
387
  var normalizeDomain = (value) => {
239
388
  const trimmed = value.trim();
240
389
  const parsed = trimmed.startsWith("http") ? new URL(trimmed) : new URL(`https://${trimmed}`);
@@ -248,16 +397,6 @@ var decodeIssuer = (token) => {
248
397
  return void 0;
249
398
  }
250
399
  };
251
- var ensureOpenIdScope = (scope) => {
252
- if (!scope) {
253
- return DEFAULT_SCOPES;
254
- }
255
- const scopes = scope.split(" ");
256
- if (!scopes.includes("openid")) {
257
- scopes.unshift("openid");
258
- }
259
- return scopes.join(" ");
260
- };
261
400
  var ServerClient = class {
262
401
  #options;
263
402
  #transactionStore;
@@ -268,6 +407,7 @@ var ServerClient = class {
268
407
  #staticDomain;
269
408
  #authClient;
270
409
  #mfaClient;
410
+ #passkeyClient;
271
411
  /**
272
412
  * The underlying `authClient` instance that can be used to interact with the Auth0 Authentication API.
273
413
  * Generally, you should prefer to use the higher-level methods exposed on the `ServerClient` instance.
@@ -302,6 +442,19 @@ var ServerClient = class {
302
442
  }
303
443
  return this.#mfaClient;
304
444
  }
445
+ /**
446
+ * The passkey client for signing up and logging in users with WebAuthn credentials.
447
+ *
448
+ * Provides `register()` and `challenge()` to request signup/login challenges, and
449
+ * `getToken()` to exchange the resulting credential for tokens and persist the session.
450
+ *
451
+ * Unlike `mfa`, this property is available in both static and resolver (multi-tenant)
452
+ * domain modes. In resolver mode, pass the same `storeOptions` to `register()`/`challenge()`
453
+ * and `getToken()` so the credential is exchanged against the tenant that issued it.
454
+ */
455
+ get passkey() {
456
+ return this.#passkeyClient;
457
+ }
305
458
  constructor(options) {
306
459
  this.#options = options;
307
460
  this.#stateStoreIdentifier = this.#options.stateIdentifier || "__a0_session";
@@ -343,6 +496,14 @@ var ServerClient = class {
343
496
  defaultAudience: this.#options.authorizationParams?.audience ?? "default"
344
497
  });
345
498
  }
499
+ this.#passkeyClient = new ServerPasskeyClient({
500
+ resolveDomain: (storeOptions) => this.#resolveDomain(storeOptions),
501
+ getAuthClient: (domain) => this.#getAuthClient(domain),
502
+ stateStore: this.#stateStore,
503
+ stateStoreIdentifier: this.#stateStoreIdentifier,
504
+ defaultScope: this.#options.authorizationParams?.scope,
505
+ defaultAudience: this.#options.authorizationParams?.audience
506
+ });
346
507
  }
347
508
  async #resolveDomain(storeOptions) {
348
509
  if (typeof this.#options.domain === "function") {
@@ -436,6 +597,7 @@ var ServerClient = class {
436
597
  *
437
598
  * @throws {MissingTransactionError} When no transaction was found.
438
599
  * @throws {TokenByCodeError} If there was an issue requesting the access token.
600
+ * @throws {SessionExpiredError} When the ID token's `session_expiry` is already in the past at login (the session is born expired); nothing is persisted.
439
601
  *
440
602
  * @returns A promise resolving to an object, containing the original appState (if present) and the authorizationDetails (when RAR was used).
441
603
  */
@@ -447,14 +609,18 @@ var ServerClient = class {
447
609
  const domain = transactionData.domain ?? await this.#resolveDomain(storeOptions);
448
610
  const authClient = this.#getAuthClient(domain);
449
611
  const tokenEndpointResponse = await authClient.getTokenByCode(url, {
612
+ // TransactionData.codeVerifier is optional only to accommodate magic-link transactions.
450
613
  codeVerifier: transactionData.codeVerifier
451
614
  });
615
+ await this.#transactionStore.delete(this.#transactionStoreIdentifier, storeOptions);
452
616
  const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
453
- const stateData = updateStateData(transactionData.audience ?? "default", existingStateData, tokenEndpointResponse, {
454
- domain
455
- });
617
+ const stateData = applySessionExpiryAtLogin(
618
+ updateStateData(transactionData.audience ?? "default", existingStateData, tokenEndpointResponse, {
619
+ domain
620
+ }),
621
+ tokenEndpointResponse.claims
622
+ );
456
623
  await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
457
- await this.#transactionStore.delete(this.#transactionStoreIdentifier, storeOptions);
458
624
  return { appState: transactionData.appState, authorizationDetails: tokenEndpointResponse.authorizationDetails };
459
625
  }
460
626
  /**
@@ -464,6 +630,7 @@ var ServerClient = class {
464
630
  *
465
631
  * @throws {MissingSessionError} If there is no active session.
466
632
  * @throws {BuildLinkUserUrlError} If there was an issue when building the Authorization URL.
633
+ * @throws {SessionExpiredError} When the session's `session_expiry` ceiling has been reached; the session is cleared and re-authentication is required.
467
634
  *
468
635
  * @returns A promise resolving to a URL object, representing the URL to redirect the user-agent to to request authorization at Auth0.
469
636
  */
@@ -480,6 +647,10 @@ var ServerClient = class {
480
647
  throw new MissingSessionError("Session domain does not match the current domain.");
481
648
  }
482
649
  }
650
+ if (isSessionExpiryReached(stateData.sessionExpiresAt)) {
651
+ await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
652
+ throw new SessionExpiredError();
653
+ }
483
654
  const domain = this.#getSessionDomain(stateData);
484
655
  const authClient = this.#getAuthClient(domain);
485
656
  const { linkUserUrl, codeVerifier } = await authClient.buildLinkUserUrl({
@@ -523,6 +694,7 @@ var ServerClient = class {
523
694
  *
524
695
  * @throws {MissingSessionError} If there is no active session.
525
696
  * @throws {BuildUnlinkUserUrlError} If there was an issue when building the User Unlinking URL.
697
+ * @throws {SessionExpiredError} When the session's `session_expiry` ceiling has been reached; the session is cleared and re-authentication is required.
526
698
  *
527
699
  * @returns A promise resolving to a URL object, representing the URL to redirect the user-agent to to request authorization at Auth0.
528
700
  */
@@ -539,6 +711,10 @@ var ServerClient = class {
539
711
  throw new MissingSessionError("Session domain does not match the current domain.");
540
712
  }
541
713
  }
714
+ if (isSessionExpiryReached(stateData.sessionExpiresAt)) {
715
+ await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
716
+ throw new SessionExpiredError();
717
+ }
542
718
  const domain = this.#getSessionDomain(stateData);
543
719
  const authClient = this.#getAuthClient(domain);
544
720
  const { unlinkUserUrl, codeVerifier } = await authClient.buildUnlinkUserUrl({
@@ -583,6 +759,7 @@ var ServerClient = class {
583
759
  * @param storeOptions Optional options used to pass to the Transaction and State Store.
584
760
  *
585
761
  * @throws {BackchannelAuthenticationError} If there was an issue when doing backchannel authentication.
762
+ * @throws {SessionExpiredError} When the ID token's `session_expiry` is already in the past at login (the session is born expired); nothing is persisted.
586
763
  *
587
764
  * @returns A promise resolving to an object, containing the authorizationDetails (when RAR was used).
588
765
  */
@@ -599,6 +776,135 @@ var ServerClient = class {
599
776
  }
600
777
  });
601
778
  const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
779
+ const stateData = applySessionExpiryAtLogin(
780
+ updateStateData(this.#options.authorizationParams?.audience ?? "default", existingStateData, tokenEndpointResponse, {
781
+ domain
782
+ }),
783
+ tokenEndpointResponse.claims
784
+ );
785
+ await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
786
+ return {
787
+ authorizationDetails: tokenEndpointResponse.authorizationDetails
788
+ };
789
+ }
790
+ /**
791
+ * Starts a passwordless flow by sending a one-time code (OTP) or a magic link.
792
+ *
793
+ * Discriminated on `connection` (and, for email, `send`) to mirror the
794
+ * `@auth0/nextjs-auth0` `passwordless.start()` surface:
795
+ * - `{ connection: 'email' }` / `{ connection: 'email', send: 'code' }` — email OTP
796
+ * - `{ connection: 'email', send: 'link', redirectUri }` — email magic link
797
+ * - `{ connection: 'sms' }` — SMS OTP
798
+ *
799
+ * OTP modes are a stateless passthrough to the Authentication API (no session, no transaction);
800
+ * complete them with {@link ServerClient#completePasswordless}.
801
+ *
802
+ * Magic-link mode is stateful: the SDK generates an opaque anti-forgery `state`, sends the link
803
+ * with the OAuth parameters embedded (`redirect_uri`, `response_type=code`, `scope`, `state`),
804
+ * and persists a transaction carrying that `state`. NO PKCE challenge is registered, so the
805
+ * transaction holds no `codeVerifier`. Complete it with
806
+ * {@link ServerClient#completePasswordlessMagicLink}. Requires the tenant setting
807
+ * `allow_magiclink_verify_without_session: true` for server-side completion.
808
+ *
809
+ * @param options Discriminated start options.
810
+ * @param storeOptions Optional options passed to the resolver / stores.
811
+ *
812
+ * @throws {PasswordlessStartError} If the request fails, or if a magic link is requested without a `redirectUri`.
813
+ *
814
+ * @example
815
+ * // Email OTP
816
+ * await serverClient.startPasswordless({ connection: 'email', email: 'user@example.com' });
817
+ * // SMS OTP
818
+ * await serverClient.startPasswordless({ connection: 'sms', phoneNumber: '+14155550100' });
819
+ * // Email magic link
820
+ * await serverClient.startPasswordless({
821
+ * connection: 'email',
822
+ * email: 'user@example.com',
823
+ * send: 'link',
824
+ * redirectUri: 'https://app.example.com/auth/callback',
825
+ * });
826
+ */
827
+ async startPasswordless(options, storeOptions) {
828
+ const domain = await this.#resolveDomain(storeOptions);
829
+ const authClient = this.#getAuthClient(domain);
830
+ if (options.connection === "sms") {
831
+ await authClient.passwordless.sendSms({
832
+ phoneNumber: options.phoneNumber,
833
+ language: options.language
834
+ });
835
+ return;
836
+ }
837
+ if (options.send !== "link") {
838
+ await authClient.passwordless.sendEmail({
839
+ email: options.email,
840
+ send: "code",
841
+ language: options.language
842
+ });
843
+ return;
844
+ }
845
+ if (!options.redirectUri || typeof options.redirectUri !== "string") {
846
+ throw new PasswordlessStartError("redirectUri is required to start a passwordless magic-link login.");
847
+ }
848
+ const state = crypto.randomUUID();
849
+ const scope = ensureOpenIdScope(options.scope ?? this.#options.authorizationParams?.scope);
850
+ const audience = options.audience ?? this.#options.authorizationParams?.audience;
851
+ await authClient.passwordless.sendEmail({
852
+ email: options.email,
853
+ send: "link",
854
+ language: options.language,
855
+ authParams: {
856
+ ...options.authParams,
857
+ redirect_uri: options.redirectUri,
858
+ response_type: "code",
859
+ scope,
860
+ ...audience ? { audience } : {},
861
+ state
862
+ }
863
+ });
864
+ const transactionState = {
865
+ audience,
866
+ domain,
867
+ state
868
+ };
869
+ await this.#transactionStore.set(this.#transactionStoreIdentifier, transactionState, false, storeOptions);
870
+ }
871
+ /**
872
+ * Completes a passwordless OTP login and persists the resulting session.
873
+ *
874
+ * Discriminated on `connection` to mirror the `@auth0/nextjs-auth0` `passwordless.verify()`
875
+ * surface. Non-redirect flow: no PKCE and no transaction store (mirrors
876
+ * {@link ServerClient#loginBackchannel}). The `openid` scope is always ensured by this layer.
877
+ *
878
+ * Note: the state store is read-then-written; if your deployment performs concurrent
879
+ * logins for the same session identifier, use a state store with atomic/serializable
880
+ * writes to avoid last-write-wins races.
881
+ *
882
+ * @param options Discriminated completion options (`connection`, identifier, `verificationCode`).
883
+ * @param storeOptions Optional options passed to the resolver / stores.
884
+ *
885
+ * @throws {PasswordlessVerifyError} If the code is invalid, expired, or rate-limited. When the
886
+ * connection requires MFA, the server responds with `mfa_required`; narrow the thrown error
887
+ * with `isMfaRequiredError(error)` to read `cause.mfa_token`.
888
+ *
889
+ * @returns A promise resolving to the authorizationDetails (when RAR was used).
890
+ */
891
+ async completePasswordless(options, storeOptions) {
892
+ const scope = ensureOpenIdScope(options.authorizationParams?.scope ?? this.#options.authorizationParams?.scope);
893
+ const audience = options.authorizationParams?.audience ?? this.#options.authorizationParams?.audience;
894
+ const domain = await this.#resolveDomain(storeOptions);
895
+ const authClient = this.#getAuthClient(domain);
896
+ const tokenEndpointResponse = options.connection === "sms" ? await authClient.getTokenByPasswordlessSms({
897
+ phoneNumber: options.phoneNumber,
898
+ code: options.verificationCode,
899
+ audience,
900
+ scope
901
+ }) : await authClient.getTokenByPasswordlessEmail({
902
+ email: options.email,
903
+ code: options.verificationCode,
904
+ audience,
905
+ scope
906
+ });
907
+ const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
602
908
  const stateData = updateStateData(
603
909
  this.#options.authorizationParams?.audience ?? "default",
604
910
  existingStateData,
@@ -610,6 +916,49 @@ var ServerClient = class {
610
916
  authorizationDetails: tokenEndpointResponse.authorizationDetails
611
917
  };
612
918
  }
919
+ /**
920
+ * Completes a passwordless magic-link login and persists the resulting session.
921
+ *
922
+ * Loads the transaction persisted by {@link ServerClient#startPasswordless} (magic-link mode), validates the
923
+ * `state` returned on the callback URL against the stored `state` (anti-forgery binding), exchanges
924
+ * the authorization code WITHOUT PKCE, writes the session, and deletes the transaction. The existing
925
+ * interactive login path ({@link ServerClient#completeInteractiveLogin}) is not used.
926
+ *
927
+ * @param url The callback URL containing the authorization `code` and `state`.
928
+ * @param storeOptions Optional options passed to the resolver / stores.
929
+ *
930
+ * @throws {MissingTransactionError} If no magic-link transaction was found.
931
+ * @throws {PasswordlessVerifyError} If the returned `state` is missing or does not match.
932
+ * @throws {TokenByCodeError} If the token exchange fails.
933
+ *
934
+ * @returns A promise resolving to the authorizationDetails (when RAR was used).
935
+ *
936
+ * @example
937
+ * const result = await serverClient.completePasswordlessMagicLink(callbackUrl, storeOptions);
938
+ */
939
+ async completePasswordlessMagicLink(url, storeOptions) {
940
+ const transactionData = await this.#transactionStore.get(this.#transactionStoreIdentifier, storeOptions);
941
+ if (!transactionData) {
942
+ throw new MissingTransactionError();
943
+ }
944
+ const expectedState = typeof transactionData.state === "string" ? transactionData.state : void 0;
945
+ const returnedState = url.searchParams.get("state");
946
+ if (!returnedState || !expectedState || returnedState !== expectedState) {
947
+ throw new PasswordlessVerifyError("State mismatch on magic-link callback");
948
+ }
949
+ const domain = transactionData.domain ?? await this.#resolveDomain(storeOptions);
950
+ const authClient = this.#getAuthClient(domain);
951
+ const tokenEndpointResponse = await authClient.getTokenByMagicLinkCode(url, { expectedState });
952
+ const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
953
+ const stateData = updateStateData(transactionData.audience ?? "default", existingStateData, tokenEndpointResponse, {
954
+ domain
955
+ });
956
+ await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
957
+ await this.#transactionStore.delete(this.#transactionStoreIdentifier, storeOptions);
958
+ return {
959
+ authorizationDetails: tokenEndpointResponse.authorizationDetails
960
+ };
961
+ }
613
962
  /**
614
963
  * Retrieves the user from the store, or undefined if no user found.
615
964
  * @param storeOptions Optional options used to pass to the Transaction and State Store.
@@ -626,6 +975,10 @@ var ServerClient = class {
626
975
  return;
627
976
  }
628
977
  }
978
+ if (isSessionExpiryReached(stateData.sessionExpiresAt)) {
979
+ await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
980
+ return;
981
+ }
629
982
  return stateData.user;
630
983
  }
631
984
  /**
@@ -642,6 +995,10 @@ var ServerClient = class {
642
995
  return;
643
996
  }
644
997
  }
998
+ if (isSessionExpiryReached(stateData.sessionExpiresAt)) {
999
+ await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
1000
+ return;
1001
+ }
645
1002
  const { internal, ...sessionData } = stateData;
646
1003
  return sessionData;
647
1004
  }
@@ -649,16 +1006,30 @@ var ServerClient = class {
649
1006
  /**
650
1007
  * Retrieves the access token from the store, or calls Auth0 when the access token is expired and a refresh token is available in the store.
651
1008
  * Also updates the store when a new token was retrieved from Auth0.
1009
+ *
1010
+ * When `options.audience` and/or `options.scope` are provided, the SDK uses the session's refresh token to
1011
+ * request an access token for that audience/scope (Multi-Resource Refresh Tokens). Tokens are cached per
1012
+ * audience and scope combination.
1013
+ *
1014
+ * @param options Optional options for requesting a specific audience/scope.
652
1015
  * @param storeOptions Optional options used to pass to the Transaction and State Store.
653
1016
  *
654
1017
  * @throws {TokenByRefreshTokenError} If the refresh token was not found or there was an issue requesting the access token. When the cause is `mfa_required`, use `isMfaRequiredError(error)` to narrow the error and read `cause.mfa_token`.
1018
+ * @throws {SessionExpiredError} When the session's `session_expiry` ceiling has been reached; the session is cleared and no refresh is attempted — the user must re-authenticate.
655
1019
  *
656
1020
  * @returns The Token Set, containing the access token, as well as additional information.
657
1021
  */
658
- async getAccessToken(storeOptions) {
659
- const stateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
660
- const audience = this.#options.authorizationParams?.audience ?? "default";
661
- const scope = this.#options.authorizationParams?.scope;
1022
+ async getAccessToken(tokenOptionsOrStoreOptions, storeOptions) {
1023
+ const hasTokenOptions = (
1024
+ // If second arg exists, first arg must be GetAccessTokenOptions
1025
+ storeOptions !== void 0 || // OR if first arg has audience/scope properties
1026
+ !!tokenOptionsOrStoreOptions && typeof tokenOptionsOrStoreOptions === "object" && ("audience" in tokenOptionsOrStoreOptions || "scope" in tokenOptionsOrStoreOptions)
1027
+ );
1028
+ const [resolvedOptions, resolvedStoreOptions] = hasTokenOptions ? [tokenOptionsOrStoreOptions, storeOptions] : [void 0, tokenOptionsOrStoreOptions];
1029
+ const stateData = await this.#stateStore.get(this.#stateStoreIdentifier, resolvedStoreOptions);
1030
+ const requestedAudience = resolvedOptions?.audience ?? this.#options.authorizationParams?.audience;
1031
+ const audience = requestedAudience ?? "default";
1032
+ const scope = resolvedOptions?.scope ?? this.#options.authorizationParams?.scope;
662
1033
  const sessionDomain = stateData ? this.#getSessionDomain(stateData) : this.#staticDomain;
663
1034
  if (this.#isResolverMode()) {
664
1035
  if (!stateData) {
@@ -667,11 +1038,15 @@ var ServerClient = class {
667
1038
  if (!sessionDomain) {
668
1039
  throw new MissingSessionError("Session domain does not match the current domain.");
669
1040
  }
670
- const resolvedDomain = await this.#resolveDomain(storeOptions);
1041
+ const resolvedDomain = await this.#resolveDomain(resolvedStoreOptions);
671
1042
  if (sessionDomain !== resolvedDomain) {
672
1043
  throw new MissingSessionError("Session domain does not match the current domain.");
673
1044
  }
674
1045
  }
1046
+ if (stateData && isSessionExpiryReached(stateData.sessionExpiresAt)) {
1047
+ await this.#stateStore.delete(this.#stateStoreIdentifier, resolvedStoreOptions);
1048
+ throw new SessionExpiredError();
1049
+ }
675
1050
  const tokenSet = stateData?.tokenSets.find(
676
1051
  (tokenSet2) => tokenSet2.audience === audience && (!scope || compareScopes(tokenSet2.scope, scope))
677
1052
  );
@@ -684,14 +1059,21 @@ var ServerClient = class {
684
1059
  );
685
1060
  }
686
1061
  const domainForSession = sessionDomain;
687
- const tokenEndpointResponse = await this.#getAuthClient(domainForSession).getTokenByRefreshToken({
688
- refreshToken: stateData.refreshToken
689
- });
690
- const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
1062
+ const tokenByRefreshTokenOptions = {
1063
+ refreshToken: stateData.refreshToken,
1064
+ // Only forward audience/scope to Auth0 when token options were explicitly supplied, and
1065
+ // never send the synthetic 'default' cache-key audience as a real request parameter.
1066
+ ...hasTokenOptions && {
1067
+ ...requestedAudience && { audience: requestedAudience },
1068
+ ...scope && { scope }
1069
+ }
1070
+ };
1071
+ const tokenEndpointResponse = await this.#getAuthClient(domainForSession).getTokenByRefreshToken(tokenByRefreshTokenOptions);
1072
+ const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, resolvedStoreOptions);
691
1073
  const updatedStateData = updateStateData(audience, existingStateData, tokenEndpointResponse, {
692
1074
  domain: domainForSession
693
1075
  });
694
- await this.#stateStore.set(this.#stateStoreIdentifier, updatedStateData, false, storeOptions);
1076
+ await this.#stateStore.set(this.#stateStoreIdentifier, updatedStateData, false, resolvedStoreOptions);
695
1077
  return {
696
1078
  accessToken: tokenEndpointResponse.accessToken,
697
1079
  scope: tokenEndpointResponse.scope,
@@ -1039,14 +1421,14 @@ var AbstractSessionStore = class extends AbstractStateStore {
1039
1421
  }
1040
1422
  /**
1041
1423
  * calculateMaxAge calculates the max age of the session based on createdAt and the rolling and absolute durations.
1424
+ * When sessionExpiresAt is provided, caps the maxAge to not exceed the time until that ceiling.
1042
1425
  */
1043
- calculateMaxAge(createdAt) {
1044
- if (!this.#rolling) {
1045
- return this.#absoluteDuration;
1046
- }
1426
+ calculateMaxAge(createdAt, sessionExpiresAt) {
1047
1427
  const now = Date.now() / 1e3 | 0;
1048
- const expiresAt = Math.min(now + this.#inactivityDuration, createdAt + this.#absoluteDuration);
1049
- const maxAge = expiresAt - now;
1428
+ let maxAge = this.#rolling ? Math.min(now + this.#inactivityDuration, createdAt + this.#absoluteDuration) - now : this.#absoluteDuration;
1429
+ if (sessionExpiresAt !== void 0) {
1430
+ maxAge = Math.min(maxAge, sessionExpiresAt - now);
1431
+ }
1050
1432
  return maxAge > 0 ? maxAge : 0;
1051
1433
  }
1052
1434
  };
@@ -1074,7 +1456,7 @@ var StatefulStateStore = class extends AbstractSessionStore {
1074
1456
  sessionId = generateId();
1075
1457
  }
1076
1458
  sessionId ??= generateId();
1077
- const maxAge = this.calculateMaxAge(stateData.internal.createdAt);
1459
+ const maxAge = this.calculateMaxAge(stateData.internal.createdAt, stateData.sessionExpiresAt);
1078
1460
  const cookieOpts = this.#getCookieOptions({
1079
1461
  maxAge
1080
1462
  });
@@ -1139,7 +1521,7 @@ var StatelessStateStore = class extends AbstractSessionStore {
1139
1521
  this.#cookieHandler = cookieHandler;
1140
1522
  }
1141
1523
  async set(identifier, stateData, removeIfExists, options) {
1142
- const maxAge = this.calculateMaxAge(stateData.internal.createdAt);
1524
+ const maxAge = this.calculateMaxAge(stateData.internal.createdAt, stateData.sessionExpiresAt);
1143
1525
  const cookieOpts = this.#getCookieOptions({
1144
1526
  maxAge
1145
1527
  });
@@ -1203,6 +1585,14 @@ import {
1203
1585
  MfaVerifyError,
1204
1586
  isMfaRequiredError
1205
1587
  } from "@auth0/auth0-auth-js";
1588
+
1589
+ // src/passkey/index.ts
1590
+ import {
1591
+ PasskeyRegisterError,
1592
+ PasskeyChallengeError,
1593
+ PasskeyGetTokenError,
1594
+ OrganizationValidationError
1595
+ } from "@auth0/auth0-auth-js";
1206
1596
  export {
1207
1597
  AbstractStateStore,
1208
1598
  AbstractTransactionStore,
@@ -1218,8 +1608,14 @@ export {
1218
1608
  MissingRequiredArgumentError,
1219
1609
  MissingSessionError,
1220
1610
  MissingTransactionError,
1611
+ OrganizationValidationError,
1612
+ PasskeyChallengeError,
1613
+ PasskeyGetTokenError,
1614
+ PasskeyRegisterError,
1221
1615
  ServerClient,
1222
1616
  ServerMfaClient,
1617
+ ServerPasskeyClient,
1618
+ SessionExpiredError,
1223
1619
  StartLinkUserError,
1224
1620
  StatefulStateStore,
1225
1621
  StatelessStateStore,