@auth0/auth0-server-js 1.6.1 → 1.7.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
@@ -122,10 +122,24 @@ function updateStateDataForConnectionTokenSet(options, stateData, tokenEndpointR
122
122
  import {
123
123
  TokenForConnectionError,
124
124
  AuthClient,
125
+ PasswordlessStartError,
126
+ PasswordlessVerifyError,
125
127
  TokenByRefreshTokenError
126
128
  } from "@auth0/auth0-auth-js";
127
129
 
128
130
  // src/utils.ts
131
+ var DEFAULT_SCOPES = "openid profile email offline_access";
132
+ var ensureOpenIdScope = (scope) => {
133
+ const normalizedScope = scope?.trim();
134
+ if (!normalizedScope) {
135
+ return DEFAULT_SCOPES;
136
+ }
137
+ const scopes = normalizedScope.split(/\s+/);
138
+ if (!scopes.includes("openid")) {
139
+ scopes.unshift("openid");
140
+ }
141
+ return scopes.join(" ");
142
+ };
129
143
  var compareScopes = (scopes, requiredScopes) => {
130
144
  if (scopes === requiredScopes) {
131
145
  return true;
@@ -149,7 +163,7 @@ function getTelemetryConfig(config) {
149
163
  return {
150
164
  enabled: true,
151
165
  name: config?.name ?? "@auth0/auth0-server-js",
152
- version: config?.version ?? "1.6.1"
166
+ version: config?.version ?? "1.7.0"
153
167
  };
154
168
  }
155
169
 
@@ -233,8 +247,99 @@ var ServerMfaClient = class {
233
247
  }
234
248
  };
235
249
 
250
+ // src/passkey/server-passkey-client.ts
251
+ var ServerPasskeyClient = class {
252
+ #options;
253
+ /**
254
+ * @internal
255
+ */
256
+ constructor(options) {
257
+ this.#options = options;
258
+ }
259
+ /**
260
+ * Requests a passkey signup challenge for a new user.
261
+ *
262
+ * Returns the `authSession` and the WebAuthn credential creation options
263
+ * (`authnParamsPublicKey`). The application must return these to the browser,
264
+ * pass `authnParamsPublicKey` to `navigator.credentials.create()`, and then
265
+ * call `getToken()` with the resulting credential to complete signup.
266
+ *
267
+ * This method does not create a session; no state is persisted.
268
+ *
269
+ * @param options User profile data and optional realm/organization.
270
+ * @param storeOptions Optional options used to resolve the domain (resolver mode).
271
+ *
272
+ * @throws {PasskeyRegisterError} If there was an issue requesting the signup challenge.
273
+ *
274
+ * @returns A promise resolving to the signup challenge.
275
+ */
276
+ async register(options, storeOptions) {
277
+ const domain = await this.#options.resolveDomain(storeOptions);
278
+ const authClient = this.#options.getAuthClient(domain);
279
+ return authClient.passkey.register(options);
280
+ }
281
+ /**
282
+ * Requests a passkey login challenge for an existing user.
283
+ *
284
+ * Returns the `authSession` and the WebAuthn credential request options
285
+ * (`authnParamsPublicKey`). The application must return these to the browser,
286
+ * pass `authnParamsPublicKey` to `navigator.credentials.get()`, and then
287
+ * call `getToken()` with the resulting credential to complete login.
288
+ *
289
+ * This method does not create a session; no state is persisted.
290
+ *
291
+ * @param options Optional realm/organization configuration.
292
+ * @param storeOptions Optional options used to resolve the domain (resolver mode).
293
+ *
294
+ * @throws {PasskeyChallengeError} If there was an issue requesting the login challenge.
295
+ *
296
+ * @returns A promise resolving to the login challenge.
297
+ */
298
+ async challenge(options, storeOptions) {
299
+ const domain = await this.#options.resolveDomain(storeOptions);
300
+ const authClient = this.#options.getAuthClient(domain);
301
+ return authClient.passkey.challenge(options);
302
+ }
303
+ /**
304
+ * Completes a passkey authentication flow (signup or login) by exchanging the
305
+ * WebAuthn credential for tokens, and persists the resulting session.
306
+ *
307
+ * Call this after obtaining a credential from `navigator.credentials.create()`
308
+ * (signup) or `navigator.credentials.get()` (login), passing the `authSession`
309
+ * returned by `register()` / `challenge()` together with the serialized credential.
310
+ *
311
+ * In resolver (multi-tenant) mode, pass the same `storeOptions` you passed to
312
+ * `register()` / `challenge()` so the token exchange resolves the same tenant
313
+ * that issued the `authSession`; otherwise the exchange will fail.
314
+ *
315
+ * @param options The auth session, serialized credential, and optional realm/scope/audience/organization.
316
+ * @param storeOptions Optional options used to pass to the State Store (and to resolve the domain in resolver mode).
317
+ *
318
+ * @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.
319
+ * @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.
320
+ *
321
+ * @returns A promise resolving to an object containing the authorizationDetails (when RAR was used).
322
+ */
323
+ async getToken(options, storeOptions) {
324
+ const scope = ensureOpenIdScope(options.scope ?? this.#options.defaultScope);
325
+ const audience = options.audience ?? this.#options.defaultAudience;
326
+ const domain = await this.#options.resolveDomain(storeOptions);
327
+ const authClient = this.#options.getAuthClient(domain);
328
+ const tokenEndpointResponse = await authClient.passkey.getTokenByPasskey({
329
+ ...options,
330
+ scope,
331
+ audience
332
+ });
333
+ const existingStateData = await this.#options.stateStore.get(this.#options.stateStoreIdentifier, storeOptions);
334
+ const stateData = updateStateData(audience ?? "default", existingStateData, tokenEndpointResponse, { domain });
335
+ await this.#options.stateStore.set(this.#options.stateStoreIdentifier, stateData, true, storeOptions);
336
+ return {
337
+ authorizationDetails: tokenEndpointResponse.authorizationDetails
338
+ };
339
+ }
340
+ };
341
+
236
342
  // src/server-client.ts
237
- var DEFAULT_SCOPES = "openid profile email offline_access";
238
343
  var normalizeDomain = (value) => {
239
344
  const trimmed = value.trim();
240
345
  const parsed = trimmed.startsWith("http") ? new URL(trimmed) : new URL(`https://${trimmed}`);
@@ -248,16 +353,6 @@ var decodeIssuer = (token) => {
248
353
  return void 0;
249
354
  }
250
355
  };
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
356
  var ServerClient = class {
262
357
  #options;
263
358
  #transactionStore;
@@ -268,6 +363,7 @@ var ServerClient = class {
268
363
  #staticDomain;
269
364
  #authClient;
270
365
  #mfaClient;
366
+ #passkeyClient;
271
367
  /**
272
368
  * The underlying `authClient` instance that can be used to interact with the Auth0 Authentication API.
273
369
  * Generally, you should prefer to use the higher-level methods exposed on the `ServerClient` instance.
@@ -302,6 +398,19 @@ var ServerClient = class {
302
398
  }
303
399
  return this.#mfaClient;
304
400
  }
401
+ /**
402
+ * The passkey client for signing up and logging in users with WebAuthn credentials.
403
+ *
404
+ * Provides `register()` and `challenge()` to request signup/login challenges, and
405
+ * `getToken()` to exchange the resulting credential for tokens and persist the session.
406
+ *
407
+ * Unlike `mfa`, this property is available in both static and resolver (multi-tenant)
408
+ * domain modes. In resolver mode, pass the same `storeOptions` to `register()`/`challenge()`
409
+ * and `getToken()` so the credential is exchanged against the tenant that issued it.
410
+ */
411
+ get passkey() {
412
+ return this.#passkeyClient;
413
+ }
305
414
  constructor(options) {
306
415
  this.#options = options;
307
416
  this.#stateStoreIdentifier = this.#options.stateIdentifier || "__a0_session";
@@ -343,6 +452,14 @@ var ServerClient = class {
343
452
  defaultAudience: this.#options.authorizationParams?.audience ?? "default"
344
453
  });
345
454
  }
455
+ this.#passkeyClient = new ServerPasskeyClient({
456
+ resolveDomain: (storeOptions) => this.#resolveDomain(storeOptions),
457
+ getAuthClient: (domain) => this.#getAuthClient(domain),
458
+ stateStore: this.#stateStore,
459
+ stateStoreIdentifier: this.#stateStoreIdentifier,
460
+ defaultScope: this.#options.authorizationParams?.scope,
461
+ defaultAudience: this.#options.authorizationParams?.audience
462
+ });
346
463
  }
347
464
  async #resolveDomain(storeOptions) {
348
465
  if (typeof this.#options.domain === "function") {
@@ -447,6 +564,7 @@ var ServerClient = class {
447
564
  const domain = transactionData.domain ?? await this.#resolveDomain(storeOptions);
448
565
  const authClient = this.#getAuthClient(domain);
449
566
  const tokenEndpointResponse = await authClient.getTokenByCode(url, {
567
+ // TransactionData.codeVerifier is optional only to accommodate magic-link transactions.
450
568
  codeVerifier: transactionData.codeVerifier
451
569
  });
452
570
  const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
@@ -610,6 +728,178 @@ var ServerClient = class {
610
728
  authorizationDetails: tokenEndpointResponse.authorizationDetails
611
729
  };
612
730
  }
731
+ /**
732
+ * Starts a passwordless flow by sending a one-time code (OTP) or a magic link.
733
+ *
734
+ * Discriminated on `connection` (and, for email, `send`) to mirror the
735
+ * `@auth0/nextjs-auth0` `passwordless.start()` surface:
736
+ * - `{ connection: 'email' }` / `{ connection: 'email', send: 'code' }` — email OTP
737
+ * - `{ connection: 'email', send: 'link', redirectUri }` — email magic link
738
+ * - `{ connection: 'sms' }` — SMS OTP
739
+ *
740
+ * OTP modes are a stateless passthrough to the Authentication API (no session, no transaction);
741
+ * complete them with {@link ServerClient#completePasswordless}.
742
+ *
743
+ * Magic-link mode is stateful: the SDK generates an opaque anti-forgery `state`, sends the link
744
+ * with the OAuth parameters embedded (`redirect_uri`, `response_type=code`, `scope`, `state`),
745
+ * and persists a transaction carrying that `state`. NO PKCE challenge is registered, so the
746
+ * transaction holds no `codeVerifier`. Complete it with
747
+ * {@link ServerClient#completePasswordlessMagicLink}. Requires the tenant setting
748
+ * `allow_magiclink_verify_without_session: true` for server-side completion.
749
+ *
750
+ * @param options Discriminated start options.
751
+ * @param storeOptions Optional options passed to the resolver / stores.
752
+ *
753
+ * @throws {PasswordlessStartError} If the request fails, or if a magic link is requested without a `redirectUri`.
754
+ *
755
+ * @example
756
+ * // Email OTP
757
+ * await serverClient.startPasswordless({ connection: 'email', email: 'user@example.com' });
758
+ * // SMS OTP
759
+ * await serverClient.startPasswordless({ connection: 'sms', phoneNumber: '+14155550100' });
760
+ * // Email magic link
761
+ * await serverClient.startPasswordless({
762
+ * connection: 'email',
763
+ * email: 'user@example.com',
764
+ * send: 'link',
765
+ * redirectUri: 'https://app.example.com/auth/callback',
766
+ * });
767
+ */
768
+ async startPasswordless(options, storeOptions) {
769
+ const domain = await this.#resolveDomain(storeOptions);
770
+ const authClient = this.#getAuthClient(domain);
771
+ if (options.connection === "sms") {
772
+ await authClient.passwordless.sendSms({
773
+ phoneNumber: options.phoneNumber,
774
+ language: options.language
775
+ });
776
+ return;
777
+ }
778
+ if (options.send !== "link") {
779
+ await authClient.passwordless.sendEmail({
780
+ email: options.email,
781
+ send: "code",
782
+ language: options.language
783
+ });
784
+ return;
785
+ }
786
+ if (!options.redirectUri || typeof options.redirectUri !== "string") {
787
+ throw new PasswordlessStartError("redirectUri is required to start a passwordless magic-link login.");
788
+ }
789
+ const state = crypto.randomUUID();
790
+ const scope = ensureOpenIdScope(options.scope ?? this.#options.authorizationParams?.scope);
791
+ const audience = options.audience ?? this.#options.authorizationParams?.audience;
792
+ await authClient.passwordless.sendEmail({
793
+ email: options.email,
794
+ send: "link",
795
+ language: options.language,
796
+ authParams: {
797
+ ...options.authParams,
798
+ redirect_uri: options.redirectUri,
799
+ response_type: "code",
800
+ scope,
801
+ ...audience ? { audience } : {},
802
+ state
803
+ }
804
+ });
805
+ const transactionState = {
806
+ audience,
807
+ domain,
808
+ state
809
+ };
810
+ await this.#transactionStore.set(this.#transactionStoreIdentifier, transactionState, false, storeOptions);
811
+ }
812
+ /**
813
+ * Completes a passwordless OTP login and persists the resulting session.
814
+ *
815
+ * Discriminated on `connection` to mirror the `@auth0/nextjs-auth0` `passwordless.verify()`
816
+ * surface. Non-redirect flow: no PKCE and no transaction store (mirrors
817
+ * {@link ServerClient#loginBackchannel}). The `openid` scope is always ensured by this layer.
818
+ *
819
+ * Note: the state store is read-then-written; if your deployment performs concurrent
820
+ * logins for the same session identifier, use a state store with atomic/serializable
821
+ * writes to avoid last-write-wins races.
822
+ *
823
+ * @param options Discriminated completion options (`connection`, identifier, `verificationCode`).
824
+ * @param storeOptions Optional options passed to the resolver / stores.
825
+ *
826
+ * @throws {PasswordlessVerifyError} If the code is invalid, expired, or rate-limited. When the
827
+ * connection requires MFA, the server responds with `mfa_required`; narrow the thrown error
828
+ * with `isMfaRequiredError(error)` to read `cause.mfa_token`.
829
+ *
830
+ * @returns A promise resolving to the authorizationDetails (when RAR was used).
831
+ */
832
+ async completePasswordless(options, storeOptions) {
833
+ const scope = ensureOpenIdScope(options.authorizationParams?.scope ?? this.#options.authorizationParams?.scope);
834
+ const audience = options.authorizationParams?.audience ?? this.#options.authorizationParams?.audience;
835
+ const domain = await this.#resolveDomain(storeOptions);
836
+ const authClient = this.#getAuthClient(domain);
837
+ const tokenEndpointResponse = options.connection === "sms" ? await authClient.getTokenByPasswordlessSms({
838
+ phoneNumber: options.phoneNumber,
839
+ code: options.verificationCode,
840
+ audience,
841
+ scope
842
+ }) : await authClient.getTokenByPasswordlessEmail({
843
+ email: options.email,
844
+ code: options.verificationCode,
845
+ audience,
846
+ scope
847
+ });
848
+ const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
849
+ const stateData = updateStateData(
850
+ this.#options.authorizationParams?.audience ?? "default",
851
+ existingStateData,
852
+ tokenEndpointResponse,
853
+ { domain }
854
+ );
855
+ await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
856
+ return {
857
+ authorizationDetails: tokenEndpointResponse.authorizationDetails
858
+ };
859
+ }
860
+ /**
861
+ * Completes a passwordless magic-link login and persists the resulting session.
862
+ *
863
+ * Loads the transaction persisted by {@link ServerClient#startPasswordless} (magic-link mode), validates the
864
+ * `state` returned on the callback URL against the stored `state` (anti-forgery binding), exchanges
865
+ * the authorization code WITHOUT PKCE, writes the session, and deletes the transaction. The existing
866
+ * interactive login path ({@link ServerClient#completeInteractiveLogin}) is not used.
867
+ *
868
+ * @param url The callback URL containing the authorization `code` and `state`.
869
+ * @param storeOptions Optional options passed to the resolver / stores.
870
+ *
871
+ * @throws {MissingTransactionError} If no magic-link transaction was found.
872
+ * @throws {PasswordlessVerifyError} If the returned `state` is missing or does not match.
873
+ * @throws {TokenByCodeError} If the token exchange fails.
874
+ *
875
+ * @returns A promise resolving to the authorizationDetails (when RAR was used).
876
+ *
877
+ * @example
878
+ * const result = await serverClient.completePasswordlessMagicLink(callbackUrl, storeOptions);
879
+ */
880
+ async completePasswordlessMagicLink(url, storeOptions) {
881
+ const transactionData = await this.#transactionStore.get(this.#transactionStoreIdentifier, storeOptions);
882
+ if (!transactionData) {
883
+ throw new MissingTransactionError();
884
+ }
885
+ const expectedState = typeof transactionData.state === "string" ? transactionData.state : void 0;
886
+ const returnedState = url.searchParams.get("state");
887
+ if (!returnedState || !expectedState || returnedState !== expectedState) {
888
+ throw new PasswordlessVerifyError("State mismatch on magic-link callback");
889
+ }
890
+ const domain = transactionData.domain ?? await this.#resolveDomain(storeOptions);
891
+ const authClient = this.#getAuthClient(domain);
892
+ const tokenEndpointResponse = await authClient.getTokenByMagicLinkCode(url, { expectedState });
893
+ const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
894
+ const stateData = updateStateData(transactionData.audience ?? "default", existingStateData, tokenEndpointResponse, {
895
+ domain
896
+ });
897
+ await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
898
+ await this.#transactionStore.delete(this.#transactionStoreIdentifier, storeOptions);
899
+ return {
900
+ authorizationDetails: tokenEndpointResponse.authorizationDetails
901
+ };
902
+ }
613
903
  /**
614
904
  * Retrieves the user from the store, or undefined if no user found.
615
905
  * @param storeOptions Optional options used to pass to the Transaction and State Store.
@@ -649,16 +939,29 @@ var ServerClient = class {
649
939
  /**
650
940
  * 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
941
  * Also updates the store when a new token was retrieved from Auth0.
942
+ *
943
+ * When `options.audience` and/or `options.scope` are provided, the SDK uses the session's refresh token to
944
+ * request an access token for that audience/scope (Multi-Resource Refresh Tokens). Tokens are cached per
945
+ * audience and scope combination.
946
+ *
947
+ * @param options Optional options for requesting a specific audience/scope.
652
948
  * @param storeOptions Optional options used to pass to the Transaction and State Store.
653
949
  *
654
950
  * @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`.
655
951
  *
656
952
  * @returns The Token Set, containing the access token, as well as additional information.
657
953
  */
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;
954
+ async getAccessToken(tokenOptionsOrStoreOptions, storeOptions) {
955
+ const hasTokenOptions = (
956
+ // If second arg exists, first arg must be GetAccessTokenOptions
957
+ storeOptions !== void 0 || // OR if first arg has audience/scope properties
958
+ !!tokenOptionsOrStoreOptions && typeof tokenOptionsOrStoreOptions === "object" && ("audience" in tokenOptionsOrStoreOptions || "scope" in tokenOptionsOrStoreOptions)
959
+ );
960
+ const [resolvedOptions, resolvedStoreOptions] = hasTokenOptions ? [tokenOptionsOrStoreOptions, storeOptions] : [void 0, tokenOptionsOrStoreOptions];
961
+ const stateData = await this.#stateStore.get(this.#stateStoreIdentifier, resolvedStoreOptions);
962
+ const requestedAudience = resolvedOptions?.audience ?? this.#options.authorizationParams?.audience;
963
+ const audience = requestedAudience ?? "default";
964
+ const scope = resolvedOptions?.scope ?? this.#options.authorizationParams?.scope;
662
965
  const sessionDomain = stateData ? this.#getSessionDomain(stateData) : this.#staticDomain;
663
966
  if (this.#isResolverMode()) {
664
967
  if (!stateData) {
@@ -667,7 +970,7 @@ var ServerClient = class {
667
970
  if (!sessionDomain) {
668
971
  throw new MissingSessionError("Session domain does not match the current domain.");
669
972
  }
670
- const resolvedDomain = await this.#resolveDomain(storeOptions);
973
+ const resolvedDomain = await this.#resolveDomain(resolvedStoreOptions);
671
974
  if (sessionDomain !== resolvedDomain) {
672
975
  throw new MissingSessionError("Session domain does not match the current domain.");
673
976
  }
@@ -684,14 +987,21 @@ var ServerClient = class {
684
987
  );
685
988
  }
686
989
  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);
990
+ const tokenByRefreshTokenOptions = {
991
+ refreshToken: stateData.refreshToken,
992
+ // Only forward audience/scope to Auth0 when token options were explicitly supplied, and
993
+ // never send the synthetic 'default' cache-key audience as a real request parameter.
994
+ ...hasTokenOptions && {
995
+ ...requestedAudience && { audience: requestedAudience },
996
+ ...scope && { scope }
997
+ }
998
+ };
999
+ const tokenEndpointResponse = await this.#getAuthClient(domainForSession).getTokenByRefreshToken(tokenByRefreshTokenOptions);
1000
+ const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, resolvedStoreOptions);
691
1001
  const updatedStateData = updateStateData(audience, existingStateData, tokenEndpointResponse, {
692
1002
  domain: domainForSession
693
1003
  });
694
- await this.#stateStore.set(this.#stateStoreIdentifier, updatedStateData, false, storeOptions);
1004
+ await this.#stateStore.set(this.#stateStoreIdentifier, updatedStateData, false, resolvedStoreOptions);
695
1005
  return {
696
1006
  accessToken: tokenEndpointResponse.accessToken,
697
1007
  scope: tokenEndpointResponse.scope,
@@ -1203,6 +1513,14 @@ import {
1203
1513
  MfaVerifyError,
1204
1514
  isMfaRequiredError
1205
1515
  } from "@auth0/auth0-auth-js";
1516
+
1517
+ // src/passkey/index.ts
1518
+ import {
1519
+ PasskeyRegisterError,
1520
+ PasskeyChallengeError,
1521
+ PasskeyGetTokenError,
1522
+ OrganizationValidationError
1523
+ } from "@auth0/auth0-auth-js";
1206
1524
  export {
1207
1525
  AbstractStateStore,
1208
1526
  AbstractTransactionStore,
@@ -1218,8 +1536,13 @@ export {
1218
1536
  MissingRequiredArgumentError,
1219
1537
  MissingSessionError,
1220
1538
  MissingTransactionError,
1539
+ OrganizationValidationError,
1540
+ PasskeyChallengeError,
1541
+ PasskeyGetTokenError,
1542
+ PasskeyRegisterError,
1221
1543
  ServerClient,
1222
1544
  ServerMfaClient,
1545
+ ServerPasskeyClient,
1223
1546
  StartLinkUserError,
1224
1547
  StatefulStateStore,
1225
1548
  StatelessStateStore,