@auth0/auth0-server-js 1.10.0 → 1.11.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/README.md CHANGED
@@ -256,7 +256,47 @@ fastify.get('/auth/logout', async (request, reply) => {
256
256
  > [!IMPORTANT]
257
257
  > You will need to register the `RETURN_TO` in your Auth0 Application as an **Allowed Logout URLs** via the [Auth0 Dashboard](https://manage.auth0.com):
258
258
 
259
+ ### 6. Database Connections (Sign-up & Change Password)
259
260
 
261
+ The `ServerClient` exposes a `database` sub-client with `signUp` and `changePassword` for self-service registration and password-reset requests against an Auth0 [database connection](https://auth0.com/docs/authenticate/database-connections). These are **pure passthrough** operations to the underlying Authentication API — they do **not** read or write the session/state store, so no store options are required for the operation itself.
262
+
263
+ ```ts
264
+ import { ServerClient, SignUpError, ChangePasswordError } from '@auth0/auth0-server-js';
265
+
266
+ // Register a new user
267
+ try {
268
+ const user = await auth0Client.database.signUp({
269
+ email: 'user@example.com',
270
+ password: 'a-Str0ng-Password!',
271
+ connection: 'Username-Password-Authentication',
272
+ // Optional profile fields: username, givenName, familyName, name, nickname, picture, userMetadata
273
+ });
274
+ console.log(user.id); // normalized identifier; may be undefined if the server omits one
275
+ } catch (error) {
276
+ if (error instanceof SignUpError) {
277
+ console.error(error.code, error.message, error.cause);
278
+ }
279
+ }
280
+
281
+ // Request a password-change email
282
+ try {
283
+ const message = await auth0Client.database.changePassword({
284
+ email: 'user@example.com', // or `username: 'jane'` for username-only connections
285
+ connection: 'Username-Password-Authentication',
286
+ // Optional: organization
287
+ });
288
+ console.log(message); // plain-text confirmation from the server
289
+ } catch (error) {
290
+ if (error instanceof ChangePasswordError) {
291
+ console.error(error.code, error.message);
292
+ }
293
+ }
294
+ ```
295
+
296
+ > [!IMPORTANT]
297
+ > These call the public `/dbconnections/*` endpoints, which send only `clientId` (never a client secret). `changePassword` resolves to a **plain-text** confirmation string. Neither method reads or writes the session/state store, so you can call them outside an authenticated request context. Domain selection is separate from session state: in static mode the constructor-configured `domain` is used, and in resolver (multi-tenant) mode the `domain` resolver still runs per call — pass `storeOptions` so it can select the intended tenant.
298
+
299
+ For full options and error handling, see the [Database Connections section in the auth0-auth-js EXAMPLES.md](https://github.com/auth0/auth0-auth-js/blob/main/packages/auth0-auth-js/EXAMPLES.md#using-database-connections-sign-up--change-password) (the underlying database client is identical) and the runnable [`examples/database-conns`](https://github.com/auth0/auth0-auth-js/tree/main/examples/database-conns) sample.
260
300
 
261
301
  ## Feedback
262
302
 
package/dist/index.cjs CHANGED
@@ -39,6 +39,8 @@ __export(index_exports, {
39
39
  PasskeyChallengeError: () => import_auth0_auth_js3.PasskeyChallengeError,
40
40
  PasskeyGetTokenError: () => import_auth0_auth_js3.PasskeyGetTokenError,
41
41
  PasskeyRegisterError: () => import_auth0_auth_js3.PasskeyRegisterError,
42
+ PasswordlessStartError: () => import_auth0_auth_js5.PasswordlessStartError,
43
+ PasswordlessVerifyError: () => import_auth0_auth_js5.PasswordlessVerifyError,
42
44
  ServerClient: () => ServerClient,
43
45
  ServerDatabaseClient: () => ServerDatabaseClient,
44
46
  ServerMfaClient: () => ServerMfaClient,
@@ -49,12 +51,18 @@ __export(index_exports, {
49
51
  StatefulStateStore: () => StatefulStateStore,
50
52
  StatelessStateStore: () => StatelessStateStore,
51
53
  TokenExchangeError: () => import_auth0_auth_js5.TokenExchangeError,
54
+ TokenExchangeErrorCode: () => TokenExchangeErrorCode,
52
55
  TokenRevocationError: () => import_auth0_auth_js5.TokenRevocationError,
53
- isMfaRequiredError: () => import_auth0_auth_js2.isMfaRequiredError
56
+ isMfaRequiredError: () => import_auth0_auth_js5.isMfaRequiredError
54
57
  });
55
58
  module.exports = __toCommonJS(index_exports);
56
59
 
57
60
  // src/errors.ts
61
+ var TokenExchangeErrorCode = {
62
+ ACTOR_UNAVAILABLE: "actor_unavailable",
63
+ SETACTOR_REQUIRED: "setactor_required",
64
+ SESSION_TRANSFER_DISABLED: "session_transfer_disabled"
65
+ };
58
66
  var MissingTransactionError = class extends Error {
59
67
  code = "missing_transaction_error";
60
68
  constructor(message) {
@@ -254,7 +262,7 @@ function getTelemetryConfig(config) {
254
262
  return {
255
263
  enabled: true,
256
264
  name: config?.name ?? "@auth0/auth0-server-js",
257
- version: config?.version ?? "1.10.0"
265
+ version: config?.version ?? "1.11.0"
258
266
  };
259
267
  }
260
268
 
@@ -494,6 +502,24 @@ var decodeIssuer = (token) => {
494
502
  return void 0;
495
503
  }
496
504
  };
505
+ var ID_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:id_token";
506
+ var ID_TOKEN_EXPIRY_SKEW_SECONDS = 30;
507
+ var actorUnavailableError = (message) => {
508
+ const error = new import_auth0_auth_js.TokenExchangeError(message);
509
+ error.code = TokenExchangeErrorCode.ACTOR_UNAVAILABLE;
510
+ return error;
511
+ };
512
+ var isTokenExpired = (token) => {
513
+ try {
514
+ const { exp } = (0, import_jose.decodeJwt)(token);
515
+ if (typeof exp !== "number") {
516
+ return true;
517
+ }
518
+ return exp <= Date.now() / 1e3 + ID_TOKEN_EXPIRY_SKEW_SECONDS;
519
+ } catch {
520
+ return true;
521
+ }
522
+ };
497
523
  var ServerClient = class {
498
524
  #options;
499
525
  #transactionStore;
@@ -1418,6 +1444,170 @@ var ServerClient = class {
1418
1444
  const authClient = this.#getAuthClient(domain);
1419
1445
  return authClient.exchangeToken(options);
1420
1446
  }
1447
+ /**
1448
+ * Requests a Session Transfer Token (STT) for impersonation via session transfer (RFC 8693).
1449
+ *
1450
+ * Performs a Custom Token Exchange against the `urn:{domain}:session_transfer` audience and
1451
+ * returns the resulting STT. The audience is built from the SDK's resolved request domain, so
1452
+ * it is correct under multiple custom domains. The returned STT is opaque and single-use — hand
1453
+ * it to {@link ServerClient.buildSessionTransferRedirect} and do not decode, cache, or persist
1454
+ * it. This method writes nothing to the state store for the STT itself; the `act` claim is not
1455
+ * on the result — it only appears on the target session's tokens once the STT is redeemed.
1456
+ *
1457
+ * An actor is mandatory for an STT (this is what makes it auditable impersonation). It is
1458
+ * resolved in this order: an explicit `options.actor` wins; otherwise the current agent
1459
+ * session's ID token is used, refreshed when it has expired; if neither is available the method
1460
+ * throws before any network call.
1461
+ *
1462
+ * @param options Options including the developer-supplied `subjectToken`/`subjectTokenType` and an optional explicit `actor`.
1463
+ * @param storeOptions Optional options used to read the agent session (for the actor) and resolve the request domain.
1464
+ *
1465
+ * @throws {TokenExchangeError} With code `actor_unavailable` when no explicit actor is given and no usable session ID token can be resolved — no logged-in agent, a session that belongs to a different domain in resolver mode, or an expired ID token that cannot be refreshed (raised client-side, before any network call). With the default code when the exchange itself fails; a server-side `setactor_required` or `session_transfer_disabled` condition is surfaced via `cause.error` / `cause.error_description`.
1466
+ * @throws {MissingClientAuthError} When client credentials are not configured (STT requires a confidential client).
1467
+ * @throws {MissingRequiredArgumentError} When `subjectToken` or `subjectTokenType` is missing or blank (raised before any session read or network call).
1468
+ *
1469
+ * @returns A promise resolving to a {@link SessionTransferTokenResult} containing the STT and its metadata.
1470
+ */
1471
+ async requestSessionTransferToken(options, storeOptions) {
1472
+ if (!options.subjectToken || !options.subjectToken.trim()) {
1473
+ throw new MissingRequiredArgumentError("subjectToken");
1474
+ }
1475
+ if (!options.subjectTokenType || !options.subjectTokenType.trim()) {
1476
+ throw new MissingRequiredArgumentError("subjectTokenType");
1477
+ }
1478
+ const domain = await this.#resolveDomain(storeOptions);
1479
+ const actor = await this.#resolveSessionTransferActor(options.actor, domain, storeOptions);
1480
+ const authClient = this.#getAuthClient(domain);
1481
+ const response = await authClient.exchangeToken({
1482
+ subjectToken: options.subjectToken,
1483
+ subjectTokenType: options.subjectTokenType,
1484
+ audience: `urn:${domain}:session_transfer`,
1485
+ scope: options.scope,
1486
+ actorToken: actor.token,
1487
+ actorTokenType: actor.type,
1488
+ extra: options.extra
1489
+ });
1490
+ return {
1491
+ sessionTransferToken: response.accessToken,
1492
+ // Surface exactly what the server returned — never fabricate the URN, so a non-STT
1493
+ // response is not mislabelled as an STT.
1494
+ issuedTokenType: response.issuedTokenType ?? "",
1495
+ // `expiresAt` is NaN when the server omitted `expires_in`; fall back to 0 rather than
1496
+ // surfacing NaN to callers.
1497
+ expiresIn: Number.isFinite(response.expiresAt) ? Math.max(0, Math.floor(response.expiresAt - Date.now() / 1e3)) : 0,
1498
+ tokenType: response.tokenType,
1499
+ scope: response.scope
1500
+ };
1501
+ }
1502
+ /**
1503
+ * Builds the redirect URL that hands a Session Transfer Token (STT) to the target app's login URL.
1504
+ *
1505
+ * Returns `targetLoginUrl` with `session_transfer_token` (and `organization`, when provided)
1506
+ * appended as query parameters, URL-encoded. This performs no network call and writes nothing
1507
+ * to the session — it only builds a string. The developer hands the returned URL to their
1508
+ * framework's redirect.
1509
+ *
1510
+ * `targetLoginUrl` attaches a single-use credential, so it must be a trusted, app-controlled
1511
+ * value — never derived from untrusted input (e.g. a `returnTo`), or the token could leak to an
1512
+ * attacker host. To harden against that, the URL must be absolute and use `https:` (an `http:`
1513
+ * URL is accepted only for `localhost` / loopback, to support local development).
1514
+ *
1515
+ * @param targetLoginUrl The target app's login URL (absolute, https).
1516
+ * @param result The {@link SessionTransferTokenResult} from {@link ServerClient.requestSessionTransferToken}.
1517
+ * @param options Optional options, e.g. the `organization` to forward when the STT is org-scoped.
1518
+ *
1519
+ * @throws {MissingRequiredArgumentError} When `targetLoginUrl` is missing or blank.
1520
+ * @throws {InvalidConfigurationError} When `targetLoginUrl` is not an absolute URL, or does not use `https:` (except for loopback hosts).
1521
+ *
1522
+ * @returns A {@link URL} with the STT (and optional organization) as query parameters.
1523
+ */
1524
+ buildSessionTransferRedirect(targetLoginUrl, result, options) {
1525
+ if (!targetLoginUrl || !targetLoginUrl.trim()) {
1526
+ throw new MissingRequiredArgumentError("targetLoginUrl");
1527
+ }
1528
+ let url;
1529
+ try {
1530
+ url = new URL(targetLoginUrl);
1531
+ } catch {
1532
+ throw new InvalidConfigurationError(
1533
+ "targetLoginUrl must be an absolute URL (e.g. https://app.example.com/auth/login)."
1534
+ );
1535
+ }
1536
+ const isLoopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
1537
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopback)) {
1538
+ throw new InvalidConfigurationError(
1539
+ "targetLoginUrl must use https (http is allowed only for localhost/loopback). The session transfer token is a single-use credential and must not be sent over an insecure or untrusted URL."
1540
+ );
1541
+ }
1542
+ url.searchParams.set("session_transfer_token", result.sessionTransferToken);
1543
+ if (options?.organization !== void 0) {
1544
+ if (!options.organization.trim()) {
1545
+ throw new import_auth0_auth_js.OrganizationValidationError("organization must not be blank");
1546
+ }
1547
+ url.searchParams.set("organization", options.organization);
1548
+ }
1549
+ return url;
1550
+ }
1551
+ /**
1552
+ * Resolves the actor token for a Session Transfer Token request.
1553
+ *
1554
+ * An explicit actor wins. Otherwise the agent session's ID token is used, refreshed when it has
1555
+ * expired (and the refreshed session is persisted so the agent session stays coherent). If no
1556
+ * usable ID token can be obtained, throws a `TokenExchangeError` with code `actor_unavailable`
1557
+ * before any exchange is attempted.
1558
+ */
1559
+ async #resolveSessionTransferActor(actor, domain, storeOptions) {
1560
+ if (actor !== void 0) {
1561
+ if (!actor.token || !actor.token.trim()) {
1562
+ throw actorUnavailableError(
1563
+ "Unable to resolve an actor for the session transfer token: an explicit actor was provided but its token is blank. Pass a non-blank actor token, or omit `actor` to source it from the agent session."
1564
+ );
1565
+ }
1566
+ return { token: actor.token, type: actor.type ?? ID_TOKEN_TYPE };
1567
+ }
1568
+ const stateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
1569
+ if (!stateData || !stateData.idToken) {
1570
+ throw actorUnavailableError(
1571
+ "Unable to resolve an actor for the session transfer token: no actor was provided and there is no logged-in agent session. Pass an explicit actor or ensure the agent is logged in."
1572
+ );
1573
+ }
1574
+ if (this.#isResolverMode() && !await this.#isSessionForCurrentDomain(stateData, storeOptions)) {
1575
+ throw actorUnavailableError(
1576
+ "Unable to resolve an actor for the session transfer token: the agent session belongs to a different domain than the one resolved for this request. Pass an explicit actor or ensure the agent is logged in on this domain."
1577
+ );
1578
+ }
1579
+ if (!isTokenExpired(stateData.idToken)) {
1580
+ return { token: stateData.idToken, type: ID_TOKEN_TYPE };
1581
+ }
1582
+ if (!stateData.refreshToken) {
1583
+ throw actorUnavailableError(
1584
+ "Unable to resolve an actor for the session transfer token: the agent session ID token has expired and no refresh token is available to refresh it. Pass an explicit actor or re-authenticate the agent."
1585
+ );
1586
+ }
1587
+ const sessionDomain = this.#getSessionDomain(stateData) ?? domain;
1588
+ let tokenEndpointResponse;
1589
+ try {
1590
+ tokenEndpointResponse = await this.#getAuthClient(sessionDomain).getTokenByRefreshToken({
1591
+ refreshToken: stateData.refreshToken
1592
+ });
1593
+ } catch {
1594
+ throw actorUnavailableError(
1595
+ "Unable to resolve an actor for the session transfer token: refreshing the agent session ID token failed. Pass an explicit actor or re-authenticate the agent."
1596
+ );
1597
+ }
1598
+ if (!tokenEndpointResponse.idToken) {
1599
+ throw actorUnavailableError(
1600
+ "Unable to resolve an actor for the session transfer token: refreshing the agent session did not return an ID token. Pass an explicit actor or re-authenticate the agent."
1601
+ );
1602
+ }
1603
+ const audience = this.#options.authorizationParams?.audience ?? "default";
1604
+ const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
1605
+ const updatedStateData = updateStateData(audience, existingStateData, tokenEndpointResponse, {
1606
+ domain: sessionDomain
1607
+ });
1608
+ await this.#stateStore.set(this.#stateStoreIdentifier, updatedStateData, false, storeOptions);
1609
+ return { token: tokenEndpointResponse.idToken, type: ID_TOKEN_TYPE };
1610
+ }
1421
1611
  /**
1422
1612
  * Handles the backchannel logout process by verifying the logout token and deleting the session from the store if the logout token was considered valid.
1423
1613
  * @param logoutToken The logout token to verify and use to delete the session from the store.
@@ -1797,6 +1987,8 @@ var import_auth0_auth_js4 = require("@auth0/auth0-auth-js");
1797
1987
  PasskeyChallengeError,
1798
1988
  PasskeyGetTokenError,
1799
1989
  PasskeyRegisterError,
1990
+ PasswordlessStartError,
1991
+ PasswordlessVerifyError,
1800
1992
  ServerClient,
1801
1993
  ServerDatabaseClient,
1802
1994
  ServerMfaClient,
@@ -1807,6 +1999,7 @@ var import_auth0_auth_js4 = require("@auth0/auth0-auth-js");
1807
1999
  StatefulStateStore,
1808
2000
  StatelessStateStore,
1809
2001
  TokenExchangeError,
2002
+ TokenExchangeErrorCode,
1810
2003
  TokenRevocationError,
1811
2004
  isMfaRequiredError
1812
2005
  });