@auth0/auth0-server-js 1.9.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/dist/index.js CHANGED
@@ -1,4 +1,9 @@
1
1
  // src/errors.ts
2
+ var TokenExchangeErrorCode = {
3
+ ACTOR_UNAVAILABLE: "actor_unavailable",
4
+ SETACTOR_REQUIRED: "setactor_required",
5
+ SESSION_TRANSFER_DISABLED: "session_transfer_disabled"
6
+ };
2
7
  var MissingTransactionError = class extends Error {
3
8
  code = "missing_transaction_error";
4
9
  constructor(message) {
@@ -166,7 +171,8 @@ import {
166
171
  OrganizationValidationError,
167
172
  PasswordlessStartError,
168
173
  PasswordlessVerifyError,
169
- TokenByRefreshTokenError
174
+ TokenByRefreshTokenError,
175
+ TokenExchangeError
170
176
  } from "@auth0/auth0-auth-js";
171
177
 
172
178
  // src/utils.ts
@@ -205,7 +211,7 @@ function getTelemetryConfig(config) {
205
211
  return {
206
212
  enabled: true,
207
213
  name: config?.name ?? "@auth0/auth0-server-js",
208
- version: config?.version ?? "1.9.0"
214
+ version: config?.version ?? "1.11.0"
209
215
  };
210
216
  }
211
217
 
@@ -384,6 +390,53 @@ var ServerPasskeyClient = class {
384
390
  }
385
391
  };
386
392
 
393
+ // src/database/server-database-client.ts
394
+ var ServerDatabaseClient = class {
395
+ #options;
396
+ /**
397
+ * @internal
398
+ */
399
+ constructor(options) {
400
+ this.#options = options;
401
+ }
402
+ /**
403
+ * Registers a new user in a database connection.
404
+ *
405
+ * Delegates to the underlying `AuthClient.database.signUp` without any session
406
+ * state modification. The caller is responsible for handling the returned user
407
+ * data as needed.
408
+ *
409
+ * @param options The signup options (email, password, connection, etc.).
410
+ * @param storeOptions Optional options used to resolve the domain (resolver mode).
411
+ *
412
+ * @throws {SignUpError} If there was an issue signing the user up.
413
+ *
414
+ * @returns A promise resolving to the created user result with a normalized `id` field.
415
+ */
416
+ async signUp(options, storeOptions) {
417
+ const domain = await this.#options.resolveDomain(storeOptions);
418
+ return this.#options.getAuthClient(domain).database.signUp(options);
419
+ }
420
+ /**
421
+ * Requests a password-change email for a database connection user.
422
+ *
423
+ * Delegates to the underlying `AuthClient.database.changePassword` without any
424
+ * session state modification. The caller is responsible for informing the user
425
+ * of the sent email as needed.
426
+ *
427
+ * @param options The password change options (email, connection, organization, etc.).
428
+ * @param storeOptions Optional options used to resolve the domain (resolver mode).
429
+ *
430
+ * @throws {ChangePasswordError} If there was an issue requesting the password change.
431
+ *
432
+ * @returns A promise resolving to the server's plain-text confirmation message.
433
+ */
434
+ async changePassword(options, storeOptions) {
435
+ const domain = await this.#options.resolveDomain(storeOptions);
436
+ return this.#options.getAuthClient(domain).database.changePassword(options);
437
+ }
438
+ };
439
+
387
440
  // src/server-client.ts
388
441
  var normalizeDomain = (value) => {
389
442
  const trimmed = value.trim();
@@ -398,6 +451,24 @@ var decodeIssuer = (token) => {
398
451
  return void 0;
399
452
  }
400
453
  };
454
+ var ID_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:id_token";
455
+ var ID_TOKEN_EXPIRY_SKEW_SECONDS = 30;
456
+ var actorUnavailableError = (message) => {
457
+ const error = new TokenExchangeError(message);
458
+ error.code = TokenExchangeErrorCode.ACTOR_UNAVAILABLE;
459
+ return error;
460
+ };
461
+ var isTokenExpired = (token) => {
462
+ try {
463
+ const { exp } = decodeJwt(token);
464
+ if (typeof exp !== "number") {
465
+ return true;
466
+ }
467
+ return exp <= Date.now() / 1e3 + ID_TOKEN_EXPIRY_SKEW_SECONDS;
468
+ } catch {
469
+ return true;
470
+ }
471
+ };
401
472
  var ServerClient = class {
402
473
  #options;
403
474
  #transactionStore;
@@ -409,6 +480,7 @@ var ServerClient = class {
409
480
  #authClient;
410
481
  #mfaClient;
411
482
  #passkeyClient;
483
+ #databaseClient;
412
484
  /**
413
485
  * The underlying `authClient` instance that can be used to interact with the Auth0 Authentication API.
414
486
  * Generally, you should prefer to use the higher-level methods exposed on the `ServerClient` instance.
@@ -456,6 +528,21 @@ var ServerClient = class {
456
528
  get passkey() {
457
529
  return this.#passkeyClient;
458
530
  }
531
+ /**
532
+ * The database client for self-service sign-up and password-change requests
533
+ * against an Auth0 database connection.
534
+ *
535
+ * Provides `signUp()` to register a user and `changePassword()` to request a
536
+ * password-reset email. Both are pure passthrough operations to the Auth0
537
+ * Authentication API — they never read or write the session/state store.
538
+ *
539
+ * Like `passkey`, this property is available in both static and resolver
540
+ * (multi-tenant) domain modes. In resolver mode, pass `storeOptions` so the
541
+ * request resolves the intended tenant.
542
+ */
543
+ get database() {
544
+ return this.#databaseClient;
545
+ }
459
546
  constructor(options) {
460
547
  this.#options = options;
461
548
  this.#stateStoreIdentifier = this.#options.stateIdentifier || "__a0_session";
@@ -505,6 +592,10 @@ var ServerClient = class {
505
592
  defaultScope: this.#options.authorizationParams?.scope,
506
593
  defaultAudience: this.#options.authorizationParams?.audience
507
594
  });
595
+ this.#databaseClient = new ServerDatabaseClient({
596
+ resolveDomain: (storeOptions) => this.#resolveDomain(storeOptions),
597
+ getAuthClient: (domain) => this.#getAuthClient(domain)
598
+ });
508
599
  }
509
600
  async #resolveDomain(storeOptions) {
510
601
  if (typeof this.#options.domain === "function") {
@@ -1170,6 +1261,47 @@ var ServerClient = class {
1170
1261
  loginHint: options.loginHint
1171
1262
  };
1172
1263
  }
1264
+ /**
1265
+ * Revokes the refresh token stored in the current session, or an explicitly supplied token.
1266
+ *
1267
+ * In resolver mode, revocation only occurs when the session domain matches the domain resolved
1268
+ * for the current request. If the domains differ (or the session has no stored domain), the call
1269
+ * returns without revoking to avoid sending a token to the wrong tenant. This guard applies even
1270
+ * when a token is passed explicitly via `options.token`.
1271
+ *
1272
+ * @param options Optionally supply a token to revoke instead of reading from the session.
1273
+ * @param storeOptions Optional options passed to the StateStore.
1274
+ *
1275
+ * @throws {MissingRequiredArgumentError} If `options.token` is an empty string.
1276
+ * @throws {MissingSessionError} If no refresh token is found in the session and none was provided.
1277
+ * @throws {TokenRevocationError} If the revocation request fails.
1278
+ */
1279
+ async revokeRefreshToken(options = {}, storeOptions) {
1280
+ if (options.token !== void 0 && options.token.length === 0) {
1281
+ throw new MissingRequiredArgumentError("options.token must not be an empty string.");
1282
+ }
1283
+ let refreshToken = options.token;
1284
+ const needsStateData = !refreshToken || this.#isResolverMode();
1285
+ const stateData = needsStateData ? await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions) : void 0;
1286
+ if (!refreshToken) {
1287
+ refreshToken = stateData?.refreshToken;
1288
+ }
1289
+ if (!refreshToken) {
1290
+ throw new MissingSessionError("Unable to revoke refresh token: no refresh token found in session.");
1291
+ }
1292
+ let authClient;
1293
+ if (this.#isResolverMode()) {
1294
+ const resolvedDomain = await this.#resolveDomain(storeOptions);
1295
+ const sessionDomain = stateData ? this.#getSessionDomain(stateData) : void 0;
1296
+ if (stateData && sessionDomain !== resolvedDomain) {
1297
+ return;
1298
+ }
1299
+ authClient = this.#getAuthClient(sessionDomain ?? resolvedDomain);
1300
+ } else {
1301
+ authClient = this.authClient;
1302
+ }
1303
+ await authClient.revokeToken({ token: refreshToken, tokenTypeHint: "refresh_token" });
1304
+ }
1173
1305
  /**
1174
1306
  * Logs the user out and returns a URL to redirect the user-agent to after they log out.
1175
1307
  * @param options Options used to configure the logout process.
@@ -1178,17 +1310,26 @@ var ServerClient = class {
1178
1310
  */
1179
1311
  async logout(options, storeOptions) {
1180
1312
  if (!this.#isResolverMode()) {
1313
+ try {
1314
+ await this.revokeRefreshToken({}, storeOptions);
1315
+ } catch {
1316
+ }
1181
1317
  await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
1182
1318
  return this.authClient.buildLogoutUrl(options);
1183
1319
  }
1184
1320
  const resolvedDomain = await this.#resolveDomain(storeOptions);
1185
1321
  const authClient = this.#getAuthClient(resolvedDomain);
1186
1322
  const stateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
1187
- const sessionDomain = stateData ? this.#getSessionDomain(stateData) : void 0;
1188
1323
  if (!stateData) {
1189
1324
  return authClient.buildLogoutUrl(options);
1190
1325
  }
1191
- if (sessionDomain && sessionDomain === resolvedDomain) {
1326
+ const sessionDomain = this.#getSessionDomain(stateData);
1327
+ const domainMatches = sessionDomain === resolvedDomain;
1328
+ if (domainMatches) {
1329
+ try {
1330
+ await this.revokeRefreshToken({}, storeOptions);
1331
+ } catch {
1332
+ }
1192
1333
  await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
1193
1334
  }
1194
1335
  return authClient.buildLogoutUrl(options);
@@ -1252,6 +1393,170 @@ var ServerClient = class {
1252
1393
  const authClient = this.#getAuthClient(domain);
1253
1394
  return authClient.exchangeToken(options);
1254
1395
  }
1396
+ /**
1397
+ * Requests a Session Transfer Token (STT) for impersonation via session transfer (RFC 8693).
1398
+ *
1399
+ * Performs a Custom Token Exchange against the `urn:{domain}:session_transfer` audience and
1400
+ * returns the resulting STT. The audience is built from the SDK's resolved request domain, so
1401
+ * it is correct under multiple custom domains. The returned STT is opaque and single-use — hand
1402
+ * it to {@link ServerClient.buildSessionTransferRedirect} and do not decode, cache, or persist
1403
+ * it. This method writes nothing to the state store for the STT itself; the `act` claim is not
1404
+ * on the result — it only appears on the target session's tokens once the STT is redeemed.
1405
+ *
1406
+ * An actor is mandatory for an STT (this is what makes it auditable impersonation). It is
1407
+ * resolved in this order: an explicit `options.actor` wins; otherwise the current agent
1408
+ * session's ID token is used, refreshed when it has expired; if neither is available the method
1409
+ * throws before any network call.
1410
+ *
1411
+ * @param options Options including the developer-supplied `subjectToken`/`subjectTokenType` and an optional explicit `actor`.
1412
+ * @param storeOptions Optional options used to read the agent session (for the actor) and resolve the request domain.
1413
+ *
1414
+ * @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`.
1415
+ * @throws {MissingClientAuthError} When client credentials are not configured (STT requires a confidential client).
1416
+ * @throws {MissingRequiredArgumentError} When `subjectToken` or `subjectTokenType` is missing or blank (raised before any session read or network call).
1417
+ *
1418
+ * @returns A promise resolving to a {@link SessionTransferTokenResult} containing the STT and its metadata.
1419
+ */
1420
+ async requestSessionTransferToken(options, storeOptions) {
1421
+ if (!options.subjectToken || !options.subjectToken.trim()) {
1422
+ throw new MissingRequiredArgumentError("subjectToken");
1423
+ }
1424
+ if (!options.subjectTokenType || !options.subjectTokenType.trim()) {
1425
+ throw new MissingRequiredArgumentError("subjectTokenType");
1426
+ }
1427
+ const domain = await this.#resolveDomain(storeOptions);
1428
+ const actor = await this.#resolveSessionTransferActor(options.actor, domain, storeOptions);
1429
+ const authClient = this.#getAuthClient(domain);
1430
+ const response = await authClient.exchangeToken({
1431
+ subjectToken: options.subjectToken,
1432
+ subjectTokenType: options.subjectTokenType,
1433
+ audience: `urn:${domain}:session_transfer`,
1434
+ scope: options.scope,
1435
+ actorToken: actor.token,
1436
+ actorTokenType: actor.type,
1437
+ extra: options.extra
1438
+ });
1439
+ return {
1440
+ sessionTransferToken: response.accessToken,
1441
+ // Surface exactly what the server returned — never fabricate the URN, so a non-STT
1442
+ // response is not mislabelled as an STT.
1443
+ issuedTokenType: response.issuedTokenType ?? "",
1444
+ // `expiresAt` is NaN when the server omitted `expires_in`; fall back to 0 rather than
1445
+ // surfacing NaN to callers.
1446
+ expiresIn: Number.isFinite(response.expiresAt) ? Math.max(0, Math.floor(response.expiresAt - Date.now() / 1e3)) : 0,
1447
+ tokenType: response.tokenType,
1448
+ scope: response.scope
1449
+ };
1450
+ }
1451
+ /**
1452
+ * Builds the redirect URL that hands a Session Transfer Token (STT) to the target app's login URL.
1453
+ *
1454
+ * Returns `targetLoginUrl` with `session_transfer_token` (and `organization`, when provided)
1455
+ * appended as query parameters, URL-encoded. This performs no network call and writes nothing
1456
+ * to the session — it only builds a string. The developer hands the returned URL to their
1457
+ * framework's redirect.
1458
+ *
1459
+ * `targetLoginUrl` attaches a single-use credential, so it must be a trusted, app-controlled
1460
+ * value — never derived from untrusted input (e.g. a `returnTo`), or the token could leak to an
1461
+ * attacker host. To harden against that, the URL must be absolute and use `https:` (an `http:`
1462
+ * URL is accepted only for `localhost` / loopback, to support local development).
1463
+ *
1464
+ * @param targetLoginUrl The target app's login URL (absolute, https).
1465
+ * @param result The {@link SessionTransferTokenResult} from {@link ServerClient.requestSessionTransferToken}.
1466
+ * @param options Optional options, e.g. the `organization` to forward when the STT is org-scoped.
1467
+ *
1468
+ * @throws {MissingRequiredArgumentError} When `targetLoginUrl` is missing or blank.
1469
+ * @throws {InvalidConfigurationError} When `targetLoginUrl` is not an absolute URL, or does not use `https:` (except for loopback hosts).
1470
+ *
1471
+ * @returns A {@link URL} with the STT (and optional organization) as query parameters.
1472
+ */
1473
+ buildSessionTransferRedirect(targetLoginUrl, result, options) {
1474
+ if (!targetLoginUrl || !targetLoginUrl.trim()) {
1475
+ throw new MissingRequiredArgumentError("targetLoginUrl");
1476
+ }
1477
+ let url;
1478
+ try {
1479
+ url = new URL(targetLoginUrl);
1480
+ } catch {
1481
+ throw new InvalidConfigurationError(
1482
+ "targetLoginUrl must be an absolute URL (e.g. https://app.example.com/auth/login)."
1483
+ );
1484
+ }
1485
+ const isLoopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
1486
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopback)) {
1487
+ throw new InvalidConfigurationError(
1488
+ "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."
1489
+ );
1490
+ }
1491
+ url.searchParams.set("session_transfer_token", result.sessionTransferToken);
1492
+ if (options?.organization !== void 0) {
1493
+ if (!options.organization.trim()) {
1494
+ throw new OrganizationValidationError("organization must not be blank");
1495
+ }
1496
+ url.searchParams.set("organization", options.organization);
1497
+ }
1498
+ return url;
1499
+ }
1500
+ /**
1501
+ * Resolves the actor token for a Session Transfer Token request.
1502
+ *
1503
+ * An explicit actor wins. Otherwise the agent session's ID token is used, refreshed when it has
1504
+ * expired (and the refreshed session is persisted so the agent session stays coherent). If no
1505
+ * usable ID token can be obtained, throws a `TokenExchangeError` with code `actor_unavailable`
1506
+ * before any exchange is attempted.
1507
+ */
1508
+ async #resolveSessionTransferActor(actor, domain, storeOptions) {
1509
+ if (actor !== void 0) {
1510
+ if (!actor.token || !actor.token.trim()) {
1511
+ throw actorUnavailableError(
1512
+ "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."
1513
+ );
1514
+ }
1515
+ return { token: actor.token, type: actor.type ?? ID_TOKEN_TYPE };
1516
+ }
1517
+ const stateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
1518
+ if (!stateData || !stateData.idToken) {
1519
+ throw actorUnavailableError(
1520
+ "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."
1521
+ );
1522
+ }
1523
+ if (this.#isResolverMode() && !await this.#isSessionForCurrentDomain(stateData, storeOptions)) {
1524
+ throw actorUnavailableError(
1525
+ "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."
1526
+ );
1527
+ }
1528
+ if (!isTokenExpired(stateData.idToken)) {
1529
+ return { token: stateData.idToken, type: ID_TOKEN_TYPE };
1530
+ }
1531
+ if (!stateData.refreshToken) {
1532
+ throw actorUnavailableError(
1533
+ "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."
1534
+ );
1535
+ }
1536
+ const sessionDomain = this.#getSessionDomain(stateData) ?? domain;
1537
+ let tokenEndpointResponse;
1538
+ try {
1539
+ tokenEndpointResponse = await this.#getAuthClient(sessionDomain).getTokenByRefreshToken({
1540
+ refreshToken: stateData.refreshToken
1541
+ });
1542
+ } catch {
1543
+ throw actorUnavailableError(
1544
+ "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."
1545
+ );
1546
+ }
1547
+ if (!tokenEndpointResponse.idToken) {
1548
+ throw actorUnavailableError(
1549
+ "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."
1550
+ );
1551
+ }
1552
+ const audience = this.#options.authorizationParams?.audience ?? "default";
1553
+ const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
1554
+ const updatedStateData = updateStateData(audience, existingStateData, tokenEndpointResponse, {
1555
+ domain: sessionDomain
1556
+ });
1557
+ await this.#stateStore.set(this.#stateStoreIdentifier, updatedStateData, false, storeOptions);
1558
+ return { token: tokenEndpointResponse.idToken, type: ID_TOKEN_TYPE };
1559
+ }
1255
1560
  /**
1256
1561
  * Handles the backchannel logout process by verifying the logout token and deleting the session from the store if the logout token was considered valid.
1257
1562
  * @param logoutToken The logout token to verify and use to delete the session from the store.
@@ -1406,7 +1711,15 @@ var AbstractTransactionStore = class extends AbstractStore {
1406
1711
  };
1407
1712
 
1408
1713
  // src/index.ts
1409
- import { TokenExchangeError, MissingClientAuthError, OrganizationValidationError as OrganizationValidationError3 } from "@auth0/auth0-auth-js";
1714
+ import {
1715
+ TokenExchangeError as TokenExchangeError2,
1716
+ TokenRevocationError,
1717
+ MissingClientAuthError,
1718
+ OrganizationValidationError as OrganizationValidationError3,
1719
+ PasswordlessStartError as PasswordlessStartError2,
1720
+ PasswordlessVerifyError as PasswordlessVerifyError2,
1721
+ isMfaRequiredError as isMfaRequiredError2
1722
+ } from "@auth0/auth0-auth-js";
1410
1723
 
1411
1724
  // src/store/cookie-transaction-store.ts
1412
1725
  var CookieTransactionStore = class extends AbstractTransactionStore {
@@ -1618,10 +1931,14 @@ import {
1618
1931
  PasskeyGetTokenError,
1619
1932
  OrganizationValidationError as OrganizationValidationError2
1620
1933
  } from "@auth0/auth0-auth-js";
1934
+
1935
+ // src/database/index.ts
1936
+ import { SignUpError, ChangePasswordError } from "@auth0/auth0-auth-js";
1621
1937
  export {
1622
1938
  AbstractStateStore,
1623
1939
  AbstractTransactionStore,
1624
1940
  BackchannelLogoutError,
1941
+ ChangePasswordError,
1625
1942
  CookieTransactionStore,
1626
1943
  InvalidConfigurationError,
1627
1944
  IssuerValidationError,
@@ -1637,14 +1954,20 @@ export {
1637
1954
  PasskeyChallengeError,
1638
1955
  PasskeyGetTokenError,
1639
1956
  PasskeyRegisterError,
1957
+ PasswordlessStartError2 as PasswordlessStartError,
1958
+ PasswordlessVerifyError2 as PasswordlessVerifyError,
1640
1959
  ServerClient,
1960
+ ServerDatabaseClient,
1641
1961
  ServerMfaClient,
1642
1962
  ServerPasskeyClient,
1643
1963
  SessionExpiredError,
1964
+ SignUpError,
1644
1965
  StartLinkUserError,
1645
1966
  StatefulStateStore,
1646
1967
  StatelessStateStore,
1647
- TokenExchangeError,
1648
- isMfaRequiredError
1968
+ TokenExchangeError2 as TokenExchangeError,
1969
+ TokenExchangeErrorCode,
1970
+ TokenRevocationError,
1971
+ isMfaRequiredError2 as isMfaRequiredError
1649
1972
  };
1650
1973
  //# sourceMappingURL=index.js.map