@auth0/auth0-server-js 1.15.0 → 1.16.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 +38 -0
- package/dist/index.cjs +501 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +519 -59
- package/dist/index.d.ts +519 -59
- package/dist/index.js +479 -7
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -53,6 +53,22 @@ var IssuerValidationError = class extends Error {
|
|
|
53
53
|
this.name = "IssuerValidationError";
|
|
54
54
|
}
|
|
55
55
|
};
|
|
56
|
+
var MissingAnonymousSessionError = class extends Error {
|
|
57
|
+
code = "missing_anonymous_session_error";
|
|
58
|
+
constructor(message) {
|
|
59
|
+
super(message ?? "There is no anonymous session. Call `anonymous.createSession()` first.");
|
|
60
|
+
this.name = "MissingAnonymousSessionError";
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var AnonymousSessionExpiredError = class extends Error {
|
|
64
|
+
code = "anonymous_session_expired";
|
|
65
|
+
constructor(message) {
|
|
66
|
+
super(
|
|
67
|
+
message ?? "The anonymous session has expired or is no longer valid. Call `anonymous.createSession()` to start a new one."
|
|
68
|
+
);
|
|
69
|
+
this.name = "AnonymousSessionExpiredError";
|
|
70
|
+
}
|
|
71
|
+
};
|
|
56
72
|
var SessionExpiredError = class extends Error {
|
|
57
73
|
code = "session_expired";
|
|
58
74
|
constructor(message) {
|
|
@@ -203,7 +219,7 @@ var compareScopes = (scopes, requiredScopes) => {
|
|
|
203
219
|
};
|
|
204
220
|
|
|
205
221
|
// src/server-client.ts
|
|
206
|
-
import { decodeJwt } from "jose";
|
|
222
|
+
import { decodeJwt as decodeJwt2 } from "jose";
|
|
207
223
|
|
|
208
224
|
// src/telemetry.ts
|
|
209
225
|
function getTelemetryConfig(config) {
|
|
@@ -213,7 +229,7 @@ function getTelemetryConfig(config) {
|
|
|
213
229
|
return {
|
|
214
230
|
enabled: true,
|
|
215
231
|
name: config?.name ?? "@auth0/auth0-server-js",
|
|
216
|
-
version: config?.version ?? "1.
|
|
232
|
+
version: config?.version ?? "1.16.0"
|
|
217
233
|
};
|
|
218
234
|
}
|
|
219
235
|
|
|
@@ -287,6 +303,7 @@ var ServerMfaClient = class {
|
|
|
287
303
|
true,
|
|
288
304
|
storeOptions
|
|
289
305
|
);
|
|
306
|
+
await this.#options.onUserSessionEstablished?.(storeOptions);
|
|
290
307
|
const result = {
|
|
291
308
|
accessToken: tokenResponse.accessToken,
|
|
292
309
|
tokenType: tokenResponse.tokenType ?? "bearer",
|
|
@@ -395,6 +412,7 @@ var ServerPasskeyClient = class {
|
|
|
395
412
|
const existingStateData = await this.#options.stateStore.get(this.#options.stateStoreIdentifier, storeOptions);
|
|
396
413
|
const stateData = updateStateData(audience ?? "default", existingStateData, tokenEndpointResponse, { domain });
|
|
397
414
|
await this.#options.stateStore.set(this.#options.stateStoreIdentifier, stateData, true, storeOptions);
|
|
415
|
+
await this.#options.onUserSessionEstablished?.(storeOptions);
|
|
398
416
|
return {
|
|
399
417
|
authorizationDetails: tokenEndpointResponse.authorizationDetails
|
|
400
418
|
};
|
|
@@ -489,6 +507,225 @@ function applyEnterpriseConnectRestrictions(instance) {
|
|
|
489
507
|
}
|
|
490
508
|
}
|
|
491
509
|
|
|
510
|
+
// src/anonymous/server-anonymous-client.ts
|
|
511
|
+
import { decodeJwt } from "jose";
|
|
512
|
+
var DEFAULT_AUDIENCE_CACHE_KEY = "default";
|
|
513
|
+
var readAnonymousSub = (accessToken) => {
|
|
514
|
+
try {
|
|
515
|
+
const { sub } = decodeJwt(accessToken);
|
|
516
|
+
return typeof sub === "string" ? sub : void 0;
|
|
517
|
+
} catch {
|
|
518
|
+
return void 0;
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
var cacheKeyScope = (tokenSet) => tokenSet.requestedScope ?? tokenSet.scope;
|
|
522
|
+
var upsertTokenSet = (tokenSets, tokenSet) => {
|
|
523
|
+
const matches = (candidate) => candidate.audience === tokenSet.audience && cacheKeyScope(candidate) === cacheKeyScope(tokenSet);
|
|
524
|
+
return tokenSets.some(matches) ? tokenSets.map((candidate) => matches(candidate) ? tokenSet : candidate) : [...tokenSets, tokenSet];
|
|
525
|
+
};
|
|
526
|
+
var ServerAnonymousClient = class {
|
|
527
|
+
#options;
|
|
528
|
+
/**
|
|
529
|
+
* @internal
|
|
530
|
+
*/
|
|
531
|
+
constructor(options) {
|
|
532
|
+
this.#options = options;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Creates an anonymous session and stores it, returning the first anonymous access token.
|
|
536
|
+
*
|
|
537
|
+
* Replaces any anonymous session already stored for this visitor. Metadata can only be
|
|
538
|
+
* attached here, because Auth0 rejects a request that carries both metadata and an
|
|
539
|
+
* existing session.
|
|
540
|
+
*
|
|
541
|
+
* The anonymous identity (`sub`) is read off the first access token and stored alongside
|
|
542
|
+
* the metadata, so {@link ServerAnonymousClient.getSession} can hand both back without the
|
|
543
|
+
* application decoding a token. It stays `undefined` for an audience with token encryption
|
|
544
|
+
* (`token_encryption`) enabled, whose access token is an encrypted JWE only the API can
|
|
545
|
+
* read.
|
|
546
|
+
*
|
|
547
|
+
* @param options Optional audience, scope and metadata for the new session.
|
|
548
|
+
* @param storeOptions Optional options used to pass to the anonymous store (and to resolve the domain in resolver mode).
|
|
549
|
+
*
|
|
550
|
+
* @throws {AnonymousSessionError} If Auth0 rejected the request. Common codes are
|
|
551
|
+
* `feature_not_enabled` (the tenant flag is off), `unauthorized_client` (the client is
|
|
552
|
+
* not enabled for anonymous sessions), `invalid_target` (the resource server does not
|
|
553
|
+
* allow anonymous access), `invalid_request` (metadata over 1024 bytes, or not all
|
|
554
|
+
* strings) and `access_denied`. `code` is `server_error` when Auth0 answers with a body
|
|
555
|
+
* that is not JSON.
|
|
556
|
+
*
|
|
557
|
+
* Only call this once you have established the visitor has no anonymous session (check
|
|
558
|
+
* {@link ServerAnonymousClient.getSession} first), rather than on every request.
|
|
559
|
+
*
|
|
560
|
+
* @returns The anonymous access token for the requested audience.
|
|
561
|
+
*/
|
|
562
|
+
async createSession(options, storeOptions) {
|
|
563
|
+
const domain = await this.#options.resolveDomain(storeOptions);
|
|
564
|
+
const requestedAudience = options?.audience ?? this.#options.defaultAudience;
|
|
565
|
+
const audience = requestedAudience ?? DEFAULT_AUDIENCE_CACHE_KEY;
|
|
566
|
+
const scope = options?.scope;
|
|
567
|
+
const session = await this.#options.getAuthClient(domain).anonymous.createSession({
|
|
568
|
+
...requestedAudience && { audience: requestedAudience },
|
|
569
|
+
...scope && { scope },
|
|
570
|
+
...options?.metadata && { metadata: options.metadata }
|
|
571
|
+
});
|
|
572
|
+
const tokenSet = {
|
|
573
|
+
audience,
|
|
574
|
+
accessToken: session.accessToken,
|
|
575
|
+
scope: session.scope,
|
|
576
|
+
...scope && scope !== session.scope && { requestedScope: scope },
|
|
577
|
+
expiresAt: session.expiresAt
|
|
578
|
+
};
|
|
579
|
+
const stateData = {
|
|
580
|
+
sessionToken: session.sessionToken,
|
|
581
|
+
sessionTokenExpiresAt: session.sessionTokenExpiresAt,
|
|
582
|
+
sub: readAnonymousSub(session.accessToken),
|
|
583
|
+
...options?.metadata && { metadata: options.metadata },
|
|
584
|
+
createdAt: Math.floor(Date.now() / 1e3),
|
|
585
|
+
tokenSets: [tokenSet],
|
|
586
|
+
domain
|
|
587
|
+
};
|
|
588
|
+
await this.#options.anonymousStore.set(
|
|
589
|
+
this.#options.anonymousStoreIdentifier,
|
|
590
|
+
stateData,
|
|
591
|
+
true,
|
|
592
|
+
storeOptions
|
|
593
|
+
);
|
|
594
|
+
return tokenSet;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Returns an anonymous access token for the stored anonymous session, fetching a fresh
|
|
598
|
+
* one from Auth0 when the cached one has expired.
|
|
599
|
+
*
|
|
600
|
+
* Tokens are cached per audience and scope, so requesting a second audience returns a
|
|
601
|
+
* second token for the same anonymous identity without replacing the first.
|
|
602
|
+
*
|
|
603
|
+
* Auth0 may grant fewer scopes than requested — a scope the anonymous identity is not
|
|
604
|
+
* entitled to is silently dropped and the response is still a success. Always check
|
|
605
|
+
* `tokenSet.scope` before calling your API; do not assume the token carries every scope
|
|
606
|
+
* you asked for.
|
|
607
|
+
*
|
|
608
|
+
* This never creates a session. If the anonymous session has expired, the stored session
|
|
609
|
+
* is deleted and `AnonymousSessionExpiredError` is thrown, so a visitor is never moved
|
|
610
|
+
* onto a fresh anonymous identity behind your back.
|
|
611
|
+
*
|
|
612
|
+
* @param options Optional audience and scope for the requested token.
|
|
613
|
+
* @param storeOptions Optional options used to pass to the anonymous store (and to resolve the domain in resolver mode).
|
|
614
|
+
*
|
|
615
|
+
* @throws {MissingAnonymousSessionError} When there is no anonymous session stored, or the stored one belongs to another Auth0 domain (resolver mode).
|
|
616
|
+
* @throws {AnonymousSessionExpiredError} When the anonymous session has expired or Auth0 rejected the session token. The stored session is deleted first.
|
|
617
|
+
*
|
|
618
|
+
* @returns The anonymous access token for the requested audience.
|
|
619
|
+
*/
|
|
620
|
+
async getAccessToken(options, storeOptions) {
|
|
621
|
+
const stateData = await this.#options.anonymousStore.get(
|
|
622
|
+
this.#options.anonymousStoreIdentifier,
|
|
623
|
+
storeOptions
|
|
624
|
+
);
|
|
625
|
+
if (!stateData?.sessionToken) {
|
|
626
|
+
throw new MissingAnonymousSessionError();
|
|
627
|
+
}
|
|
628
|
+
const domain = await this.#options.resolveDomain(storeOptions);
|
|
629
|
+
if (this.#options.isResolverMode() && stateData.domain !== domain) {
|
|
630
|
+
throw new MissingAnonymousSessionError(
|
|
631
|
+
"The stored anonymous session was created for a different Auth0 domain."
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
const requestedAudience = options?.audience ?? this.#options.defaultAudience;
|
|
635
|
+
const audience = requestedAudience ?? DEFAULT_AUDIENCE_CACHE_KEY;
|
|
636
|
+
const scope = options?.scope;
|
|
637
|
+
const cachedTokenSet = stateData.tokenSets.find(
|
|
638
|
+
(tokenSet2) => tokenSet2.audience === audience && (!scope || cacheKeyScope(tokenSet2) === scope || compareScopes(tokenSet2.scope, scope))
|
|
639
|
+
);
|
|
640
|
+
if (cachedTokenSet && cachedTokenSet.expiresAt > Date.now() / 1e3) {
|
|
641
|
+
return cachedTokenSet;
|
|
642
|
+
}
|
|
643
|
+
const renewed = await this.#options.getAuthClient(domain).anonymous.getAccessToken({
|
|
644
|
+
sessionToken: stateData.sessionToken,
|
|
645
|
+
...requestedAudience && { audience: requestedAudience },
|
|
646
|
+
...scope && { scope }
|
|
647
|
+
});
|
|
648
|
+
if (renewed.sessionReplaced) {
|
|
649
|
+
await this.#options.anonymousStore.delete(this.#options.anonymousStoreIdentifier, storeOptions);
|
|
650
|
+
throw new AnonymousSessionExpiredError();
|
|
651
|
+
}
|
|
652
|
+
const tokenSet = {
|
|
653
|
+
audience,
|
|
654
|
+
accessToken: renewed.accessToken,
|
|
655
|
+
scope: renewed.scope,
|
|
656
|
+
...scope && scope !== renewed.scope && { requestedScope: scope },
|
|
657
|
+
expiresAt: renewed.expiresAt
|
|
658
|
+
};
|
|
659
|
+
const existingStateData = await this.#options.anonymousStore.get(
|
|
660
|
+
this.#options.anonymousStoreIdentifier,
|
|
661
|
+
storeOptions
|
|
662
|
+
);
|
|
663
|
+
if (existingStateData?.sessionToken === stateData.sessionToken) {
|
|
664
|
+
await this.#options.anonymousStore.set(
|
|
665
|
+
this.#options.anonymousStoreIdentifier,
|
|
666
|
+
{
|
|
667
|
+
...existingStateData,
|
|
668
|
+
...existingStateData.sub === void 0 && { sub: readAnonymousSub(renewed.accessToken) },
|
|
669
|
+
tokenSets: upsertTokenSet(existingStateData.tokenSets, tokenSet)
|
|
670
|
+
},
|
|
671
|
+
false,
|
|
672
|
+
storeOptions
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
return tokenSet;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Returns the stored anonymous session, or `undefined` when there is none.
|
|
679
|
+
*
|
|
680
|
+
* Two common uses:
|
|
681
|
+
* - **Gate `createSession`** — call this first; only create a session when the result is `undefined`.
|
|
682
|
+
* - **Read identity for a merge** — `sub` and `metadata` are available here without decoding a token.
|
|
683
|
+
*
|
|
684
|
+
* This is a local read of the store with no request to Auth0. It cannot tell you whether
|
|
685
|
+
* Auth0 still considers the session valid — only {@link ServerAnonymousClient.getAccessToken} can.
|
|
686
|
+
* The session token is never included in the result.
|
|
687
|
+
*
|
|
688
|
+
* @param storeOptions Optional options used to pass to the anonymous store (and to resolve the domain in resolver mode).
|
|
689
|
+
*
|
|
690
|
+
* @returns The anonymous session, or `undefined` when there is none for this visitor (or it belongs to another Auth0 domain in resolver mode).
|
|
691
|
+
*/
|
|
692
|
+
async getSession(storeOptions) {
|
|
693
|
+
const stateData = await this.#options.anonymousStore.get(
|
|
694
|
+
this.#options.anonymousStoreIdentifier,
|
|
695
|
+
storeOptions
|
|
696
|
+
);
|
|
697
|
+
if (!stateData) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
if (this.#options.isResolverMode()) {
|
|
701
|
+
const domain = await this.#options.resolveDomain(storeOptions);
|
|
702
|
+
if (stateData.domain !== domain) {
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
const { sessionToken, ...sessionData } = stateData;
|
|
707
|
+
return sessionData;
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Clears the anonymous session from the store.
|
|
711
|
+
*
|
|
712
|
+
* Does not call `POST /anonymous/logout` — that endpoint only clears the `auth0_anon`
|
|
713
|
+
* browser cookie, which your server never holds. Any access tokens already issued remain
|
|
714
|
+
* valid until they expire (~2 hours by default).
|
|
715
|
+
*
|
|
716
|
+
* You rarely need to call this directly: every method that establishes a user session
|
|
717
|
+
* clears the anonymous session automatically (unless `clearAnonymousSessionOnLogin: false`
|
|
718
|
+
* is set), and `serverClient.logout()` clears it too. Call this to explicitly reset a
|
|
719
|
+
* visitor's anonymous identity, or to clean up after your own post-login merge logic when
|
|
720
|
+
* automatic clearing is disabled.
|
|
721
|
+
*
|
|
722
|
+
* @param storeOptions Optional options used to pass to the anonymous store.
|
|
723
|
+
*/
|
|
724
|
+
async logout(storeOptions) {
|
|
725
|
+
await this.#options.anonymousStore.delete(this.#options.anonymousStoreIdentifier, storeOptions);
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
|
|
492
729
|
// src/server-client.ts
|
|
493
730
|
var normalizeDomain = (value) => {
|
|
494
731
|
const trimmed = value.trim();
|
|
@@ -497,7 +734,7 @@ var normalizeDomain = (value) => {
|
|
|
497
734
|
};
|
|
498
735
|
var decodeIssuer = (token) => {
|
|
499
736
|
try {
|
|
500
|
-
const { iss } =
|
|
737
|
+
const { iss } = decodeJwt2(token);
|
|
501
738
|
return typeof iss === "string" ? iss : void 0;
|
|
502
739
|
} catch {
|
|
503
740
|
return void 0;
|
|
@@ -512,7 +749,7 @@ var actorUnavailableError = (message) => {
|
|
|
512
749
|
};
|
|
513
750
|
var isTokenExpired = (token) => {
|
|
514
751
|
try {
|
|
515
|
-
const { exp } =
|
|
752
|
+
const { exp } = decodeJwt2(token);
|
|
516
753
|
if (typeof exp !== "number") {
|
|
517
754
|
return true;
|
|
518
755
|
}
|
|
@@ -534,6 +771,7 @@ var ServerClient = class {
|
|
|
534
771
|
#mfaClient;
|
|
535
772
|
#passkeyClient;
|
|
536
773
|
#databaseClient;
|
|
774
|
+
#anonymousClient;
|
|
537
775
|
/**
|
|
538
776
|
* The underlying `authClient` instance that can be used to interact with the Auth0 Authentication API.
|
|
539
777
|
* Generally, you should prefer to use the higher-level methods exposed on the `ServerClient` instance.
|
|
@@ -596,6 +834,42 @@ var ServerClient = class {
|
|
|
596
834
|
get database() {
|
|
597
835
|
return this.#databaseClient;
|
|
598
836
|
}
|
|
837
|
+
/**
|
|
838
|
+
* The anonymous session client, for giving a visitor who has not logged in a stable
|
|
839
|
+
* identity and an access token for your API.
|
|
840
|
+
*
|
|
841
|
+
* Provides `createSession()` to establish the anonymous identity, `getAccessToken()` to
|
|
842
|
+
* obtain and renew anonymous access tokens, `getSession()` to check whether a visitor
|
|
843
|
+
* already has one, and `logout()` to discard it.
|
|
844
|
+
*
|
|
845
|
+
* Requires `anonymousStore` on the `ServerClient`, and a tenant and client configured for
|
|
846
|
+
* anonymous sessions. Anonymous sessions are kept in that store, never in the state
|
|
847
|
+
* store, so `getSession()` and `getUser()` still report no user while one is active.
|
|
848
|
+
*
|
|
849
|
+
* The anonymous session ends when the visitor logs in: every login method clears it once
|
|
850
|
+
* the user session is written, unless you set `clearAnonymousSessionOnLogin: false`.
|
|
851
|
+
* {@link ServerClient.logout} clears it too.
|
|
852
|
+
*
|
|
853
|
+
* For `/authorize` flows, `startInteractiveLogin()` links the anonymous session to
|
|
854
|
+
* the user automatically via a Session Transfer Ticket — no extra configuration needed.
|
|
855
|
+
* For logins that bypass `/authorize` (passkey, passwordless, backchannel, custom token
|
|
856
|
+
* exchange), the anonymous session is not linked. Read `anonymous.getSession()` before
|
|
857
|
+
* the login to get the anonymous `sub` for a manual merge, or set
|
|
858
|
+
* `clearAnonymousSessionOnLogin: false` and clear it yourself afterwards.
|
|
859
|
+
*
|
|
860
|
+
* Like `passkey` and `database`, this works in both static and resolver (multi-tenant)
|
|
861
|
+
* domain modes.
|
|
862
|
+
*
|
|
863
|
+
* @throws {InvalidConfigurationError} When no `anonymousStore` is configured.
|
|
864
|
+
*/
|
|
865
|
+
get anonymous() {
|
|
866
|
+
if (!this.#anonymousClient) {
|
|
867
|
+
throw new InvalidConfigurationError(
|
|
868
|
+
"anonymous is only available when an `anonymousStore` is configured on the ServerClient."
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
return this.#anonymousClient;
|
|
872
|
+
}
|
|
599
873
|
constructor(options) {
|
|
600
874
|
this.#options = options;
|
|
601
875
|
this.#enterpriseConnect = !!options.enterpriseConnect;
|
|
@@ -654,7 +928,8 @@ var ServerClient = class {
|
|
|
654
928
|
domain,
|
|
655
929
|
stateStore: this.#stateStore,
|
|
656
930
|
stateStoreIdentifier: this.#stateStoreIdentifier,
|
|
657
|
-
defaultAudience: this.#options.authorizationParams?.audience ?? "default"
|
|
931
|
+
defaultAudience: this.#options.authorizationParams?.audience ?? "default",
|
|
932
|
+
onUserSessionEstablished: (storeOptions) => this.#clearAnonymousSessionAfterLogin(storeOptions)
|
|
658
933
|
});
|
|
659
934
|
}
|
|
660
935
|
this.#passkeyClient = new ServerPasskeyClient({
|
|
@@ -663,12 +938,26 @@ var ServerClient = class {
|
|
|
663
938
|
stateStore: this.#stateStore,
|
|
664
939
|
stateStoreIdentifier: this.#stateStoreIdentifier,
|
|
665
940
|
defaultScope: this.#options.authorizationParams?.scope,
|
|
666
|
-
defaultAudience: this.#options.authorizationParams?.audience
|
|
941
|
+
defaultAudience: this.#options.authorizationParams?.audience,
|
|
942
|
+
onUserSessionEstablished: (storeOptions) => this.#clearAnonymousSessionAfterLogin(storeOptions)
|
|
667
943
|
});
|
|
668
944
|
this.#databaseClient = new ServerDatabaseClient({
|
|
669
945
|
resolveDomain: (storeOptions) => this.#resolveDomain(storeOptions),
|
|
670
946
|
getAuthClient: (domain) => this.#getAuthClient(domain)
|
|
671
947
|
});
|
|
948
|
+
if (this.#options.anonymousStore) {
|
|
949
|
+
this.#anonymousClient = new ServerAnonymousClient({
|
|
950
|
+
resolveDomain: (storeOptions) => this.#resolveDomain(storeOptions),
|
|
951
|
+
getAuthClient: (domain) => this.#getAuthClient(domain),
|
|
952
|
+
isResolverMode: () => this.#isResolverMode(),
|
|
953
|
+
anonymousStore: this.#options.anonymousStore,
|
|
954
|
+
anonymousStoreIdentifier: this.#options.anonymousSessionIdentifier || "__a0_anon",
|
|
955
|
+
// Only the audience is inherited from the client-level authorizationParams. The
|
|
956
|
+
// scope there is written for a user session (`openid profile email offline_access`)
|
|
957
|
+
// and none of it applies to an anonymous identity.
|
|
958
|
+
defaultAudience: this.#options.authorizationParams?.audience
|
|
959
|
+
});
|
|
960
|
+
}
|
|
672
961
|
}
|
|
673
962
|
async #resolveDomain(storeOptions) {
|
|
674
963
|
if (typeof this.#options.domain === "function") {
|
|
@@ -745,6 +1034,54 @@ var ServerClient = class {
|
|
|
745
1034
|
storeOptions
|
|
746
1035
|
);
|
|
747
1036
|
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Discards the anonymous session once a login has established a user session.
|
|
1039
|
+
*
|
|
1040
|
+
* Called by every method that writes a user session, always AFTER the state store has been
|
|
1041
|
+
* written. An anonymous session outliving the login would leave a 30-day bearer credential
|
|
1042
|
+
* in a cookie on every request, and `anonymous.getAccessToken()` would keep minting
|
|
1043
|
+
* anonymous tokens for a visitor who is no longer anonymous.
|
|
1044
|
+
*
|
|
1045
|
+
* Two deliberate properties:
|
|
1046
|
+
* - **Best-effort.** The user is authenticated and their session is already persisted by
|
|
1047
|
+
* this point, so a failing anonymous store must not turn a successful login into an
|
|
1048
|
+
* error. The visitor would be unable to log in at all because of leftover state they
|
|
1049
|
+
* cannot see or clear.
|
|
1050
|
+
* - **Unconditional across domains.** In resolver (multi-tenant) mode the anonymous session
|
|
1051
|
+
* is dropped even when it was created against a different Auth0 domain than the one just
|
|
1052
|
+
* logged into. The anonymous store holds a single session per visitor, and "no anonymous
|
|
1053
|
+
* session survives a login" is a rule that can be reasoned about; a domain-dependent one
|
|
1054
|
+
* cannot.
|
|
1055
|
+
*
|
|
1056
|
+
* Skipped entirely when no `anonymousStore` is configured, or when the application opted
|
|
1057
|
+
* out with `clearAnonymousSessionOnLogin: false`.
|
|
1058
|
+
*/
|
|
1059
|
+
async #clearAnonymousSessionAfterLogin(storeOptions) {
|
|
1060
|
+
if (this.#options.clearAnonymousSessionOnLogin === false) {
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
await this.#discardAnonymousSession(storeOptions);
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Drops the anonymous session, swallowing store failures.
|
|
1067
|
+
*
|
|
1068
|
+
* Shared by the login sites and {@link ServerClient.logout}. Both run after the user
|
|
1069
|
+
* session has already been written or deleted, so the outcome the caller cares about is
|
|
1070
|
+
* settled: a failing anonymous store must not fail a login the user completed, nor block a
|
|
1071
|
+
* logout redirect and leave the visitor unable to log out. When the store is the
|
|
1072
|
+
* cookie-backed default this cannot fail, since deleting is a `Set-Cookie` on the response.
|
|
1073
|
+
*
|
|
1074
|
+
* No-op when no `anonymousStore` is configured.
|
|
1075
|
+
*/
|
|
1076
|
+
async #discardAnonymousSession(storeOptions) {
|
|
1077
|
+
if (!this.#anonymousClient) {
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
try {
|
|
1081
|
+
await this.#anonymousClient.logout(storeOptions);
|
|
1082
|
+
} catch {
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
748
1085
|
/**
|
|
749
1086
|
* Starts the interactive login process, and returns a URL to redirect the user-agent to to request authorization at Auth0.
|
|
750
1087
|
*
|
|
@@ -780,6 +1117,17 @@ var ServerClient = class {
|
|
|
780
1117
|
}
|
|
781
1118
|
const domain = await this.#resolveDomain(storeOptions);
|
|
782
1119
|
const authClient = this.#getAuthClient(domain);
|
|
1120
|
+
let anonTransferToken;
|
|
1121
|
+
if (this.#options.anonymousStore) {
|
|
1122
|
+
try {
|
|
1123
|
+
const identifier = this.#options.anonymousSessionIdentifier || "__a0_anon";
|
|
1124
|
+
const stateData = await this.#options.anonymousStore.get(identifier, storeOptions);
|
|
1125
|
+
if (stateData?.sessionToken) {
|
|
1126
|
+
anonTransferToken = await authClient.anonymous.mintTransferToken(stateData.sessionToken) ?? void 0;
|
|
1127
|
+
}
|
|
1128
|
+
} catch {
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
783
1131
|
const { codeVerifier, authorizationUrl } = await authClient.buildAuthorizationUrl({
|
|
784
1132
|
pushedAuthorizationRequests: options?.pushedAuthorizationRequests,
|
|
785
1133
|
authorizationParams: {
|
|
@@ -787,7 +1135,8 @@ var ServerClient = class {
|
|
|
787
1135
|
redirect_uri: redirectUri,
|
|
788
1136
|
scope,
|
|
789
1137
|
...resolvedOrganization ? { organization: resolvedOrganization } : {},
|
|
790
|
-
...options?.invitation ? { invitation: options.invitation } : {}
|
|
1138
|
+
...options?.invitation ? { invitation: options.invitation } : {},
|
|
1139
|
+
...anonTransferToken ? { anon_transfer_token: anonTransferToken } : {}
|
|
791
1140
|
}
|
|
792
1141
|
});
|
|
793
1142
|
const transactionState = {
|
|
@@ -857,6 +1206,7 @@ var ServerClient = class {
|
|
|
857
1206
|
tokenEndpointResponse.claims
|
|
858
1207
|
);
|
|
859
1208
|
await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
|
|
1209
|
+
await this.#clearAnonymousSessionAfterLogin(storeOptions);
|
|
860
1210
|
return {
|
|
861
1211
|
appState: transactionData.appState,
|
|
862
1212
|
authorizationDetails: tokenEndpointResponse.authorizationDetails
|
|
@@ -1024,6 +1374,7 @@ var ServerClient = class {
|
|
|
1024
1374
|
tokenEndpointResponse.claims
|
|
1025
1375
|
);
|
|
1026
1376
|
await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
|
|
1377
|
+
await this.#clearAnonymousSessionAfterLogin(storeOptions);
|
|
1027
1378
|
const result = {
|
|
1028
1379
|
authorizationDetails: tokenEndpointResponse.authorizationDetails
|
|
1029
1380
|
};
|
|
@@ -1170,6 +1521,7 @@ var ServerClient = class {
|
|
|
1170
1521
|
{ domain }
|
|
1171
1522
|
);
|
|
1172
1523
|
await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
|
|
1524
|
+
await this.#clearAnonymousSessionAfterLogin(storeOptions);
|
|
1173
1525
|
const result = {
|
|
1174
1526
|
authorizationDetails: tokenEndpointResponse.authorizationDetails
|
|
1175
1527
|
};
|
|
@@ -1220,6 +1572,7 @@ var ServerClient = class {
|
|
|
1220
1572
|
domain
|
|
1221
1573
|
});
|
|
1222
1574
|
await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
|
|
1575
|
+
await this.#clearAnonymousSessionAfterLogin(storeOptions);
|
|
1223
1576
|
await this.#transactionStore.delete(this.#transactionStoreIdentifier, storeOptions);
|
|
1224
1577
|
return {
|
|
1225
1578
|
authorizationDetails: tokenEndpointResponse.authorizationDetails
|
|
@@ -1527,6 +1880,29 @@ var ServerClient = class {
|
|
|
1527
1880
|
}
|
|
1528
1881
|
/**
|
|
1529
1882
|
* Logs the user out and returns a URL to redirect the user-agent to after they log out.
|
|
1883
|
+
*
|
|
1884
|
+
* Clears the anonymous session as well, whenever an `anonymousStore` is configured. Logging
|
|
1885
|
+
* out means the visitor is done, so leaving a 30-day anonymous credential behind in a
|
|
1886
|
+
* cookie would be surprising, and `anonymous.getAccessToken()` would keep working right
|
|
1887
|
+
* after the user logged out. This happens even when there is no user session to clear: a
|
|
1888
|
+
* visitor who only ever had an anonymous session can log out. Unlike the automatic clearing
|
|
1889
|
+
* at login, it is not affected by `clearAnonymousSessionOnLogin`, which exists so an
|
|
1890
|
+
* application can finish its post-login work.
|
|
1891
|
+
*
|
|
1892
|
+
* In resolver (multi-tenant) mode the one exception is a stored user session belonging to a
|
|
1893
|
+
* different Auth0 domain than the request resolves to. Nothing local is cleared in that
|
|
1894
|
+
* case, anonymous session included, exactly as today: that state belongs to another tenant.
|
|
1895
|
+
*
|
|
1896
|
+
* The anonymous session is only ever cleared locally. The SDK does not call
|
|
1897
|
+
* `POST /anonymous/logout`: that endpoint exists to clear the `auth0_anon` cookie in a
|
|
1898
|
+
* browser, and it revokes nothing server-side. Your server never holds that cookie, so
|
|
1899
|
+
* calling it would achieve nothing. If the visitor's browser created an anonymous session
|
|
1900
|
+
* of its own (through `@auth0/auth0-spa-js`, for example), that cookie is not covered by
|
|
1901
|
+
* this SDK and has to be cleared from the browser.
|
|
1902
|
+
*
|
|
1903
|
+
* Anonymous access tokens already handed out stay valid until they expire; there is no
|
|
1904
|
+
* anonymous session to revoke them against.
|
|
1905
|
+
*
|
|
1530
1906
|
* @param options Options used to configure the logout process.
|
|
1531
1907
|
* @param storeOptions Optional options used to pass to the Transaction and State Store.
|
|
1532
1908
|
* @param requestOptions Optional per-request options (signal, headers, customFetch). Applied to the token revocation ONLY. Building the logout URL is local string work and issues no request, so nothing here can affect it.
|
|
@@ -1547,12 +1923,14 @@ var ServerClient = class {
|
|
|
1547
1923
|
} catch {
|
|
1548
1924
|
}
|
|
1549
1925
|
await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
|
|
1926
|
+
await this.#discardAnonymousSession(storeOptions);
|
|
1550
1927
|
return this.authClient.buildLogoutUrl(options);
|
|
1551
1928
|
}
|
|
1552
1929
|
const resolvedDomain = await this.#resolveDomain(storeOptions);
|
|
1553
1930
|
const authClient = this.#getAuthClient(resolvedDomain);
|
|
1554
1931
|
const stateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
|
|
1555
1932
|
if (!stateData) {
|
|
1933
|
+
await this.#discardAnonymousSession(storeOptions);
|
|
1556
1934
|
return authClient.buildLogoutUrl(options);
|
|
1557
1935
|
}
|
|
1558
1936
|
const sessionDomain = this.#getSessionDomain(stateData);
|
|
@@ -1563,6 +1941,7 @@ var ServerClient = class {
|
|
|
1563
1941
|
} catch {
|
|
1564
1942
|
}
|
|
1565
1943
|
await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
|
|
1944
|
+
await this.#discardAnonymousSession(storeOptions);
|
|
1566
1945
|
}
|
|
1567
1946
|
return authClient.buildLogoutUrl(options);
|
|
1568
1947
|
}
|
|
@@ -1594,6 +1973,7 @@ var ServerClient = class {
|
|
|
1594
1973
|
{ domain }
|
|
1595
1974
|
);
|
|
1596
1975
|
await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
|
|
1976
|
+
await this.#clearAnonymousSessionAfterLogin(storeOptions);
|
|
1597
1977
|
const result = {
|
|
1598
1978
|
authorizationDetails: tokenEndpointResponse.authorizationDetails
|
|
1599
1979
|
};
|
|
@@ -2169,6 +2549,88 @@ var StatelessStateStore = class extends AbstractSessionStore {
|
|
|
2169
2549
|
}
|
|
2170
2550
|
};
|
|
2171
2551
|
|
|
2552
|
+
// src/store/abstract-anonymous-store.ts
|
|
2553
|
+
var AbstractAnonymousStore = class extends AbstractStore {
|
|
2554
|
+
constructor(options) {
|
|
2555
|
+
super(options);
|
|
2556
|
+
}
|
|
2557
|
+
};
|
|
2558
|
+
|
|
2559
|
+
// src/store/stateless-anonymous-store.ts
|
|
2560
|
+
var DEFAULT_ANONYMOUS_SESSION_LIFETIME = 60 * 60 * 24 * 30;
|
|
2561
|
+
var CHUNK_SIZE = 3072;
|
|
2562
|
+
var StatelessAnonymousStore = class extends AbstractAnonymousStore {
|
|
2563
|
+
#cookieOptions;
|
|
2564
|
+
#cookieHandler;
|
|
2565
|
+
#sessionTokenLifetime;
|
|
2566
|
+
constructor(options, cookieHandler) {
|
|
2567
|
+
super(options);
|
|
2568
|
+
this.#cookieOptions = options.cookie;
|
|
2569
|
+
this.#cookieHandler = cookieHandler;
|
|
2570
|
+
this.#sessionTokenLifetime = options.sessionTokenLifetime ?? DEFAULT_ANONYMOUS_SESSION_LIFETIME;
|
|
2571
|
+
}
|
|
2572
|
+
async set(identifier, anonymousStateData, removeIfExists, options) {
|
|
2573
|
+
const expiration = this.#getExpiration(anonymousStateData);
|
|
2574
|
+
const maxAge = expiration - Math.floor(Date.now() / 1e3);
|
|
2575
|
+
if (maxAge <= 0) {
|
|
2576
|
+
await this.delete(identifier, options);
|
|
2577
|
+
return;
|
|
2578
|
+
}
|
|
2579
|
+
const cookieOpts = this.#getCookieOptions({ maxAge });
|
|
2580
|
+
const encryptedStateData = await this.encrypt(identifier, anonymousStateData, expiration);
|
|
2581
|
+
const chunkCount = Math.ceil(encryptedStateData.length / CHUNK_SIZE);
|
|
2582
|
+
const chunks = [...Array(chunkCount).keys()].map((i) => ({
|
|
2583
|
+
value: encryptedStateData.substring(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE),
|
|
2584
|
+
name: `${identifier}.${i}`
|
|
2585
|
+
}));
|
|
2586
|
+
chunks.forEach((chunk) => {
|
|
2587
|
+
this.#cookieHandler.setCookie(chunk.name, chunk.value, cookieOpts, options);
|
|
2588
|
+
});
|
|
2589
|
+
const existingCookieKeys = this.#getCookieKeys(identifier, options);
|
|
2590
|
+
existingCookieKeys.filter((key) => !chunks.some((chunk) => chunk.name === key)).forEach((key) => {
|
|
2591
|
+
this.#cookieHandler.deleteCookie(key, options, this.#getCookieOptions());
|
|
2592
|
+
});
|
|
2593
|
+
}
|
|
2594
|
+
async get(identifier, options) {
|
|
2595
|
+
const cookieKeys = this.#getCookieKeys(identifier, options);
|
|
2596
|
+
const encryptedStateData = cookieKeys.map((key) => ({
|
|
2597
|
+
index: parseInt(key.split(".")[1], 10),
|
|
2598
|
+
value: this.#cookieHandler.getCookie(key, options)
|
|
2599
|
+
})).sort((a, b) => a.index - b.index).map((item) => item.value).join("");
|
|
2600
|
+
if (encryptedStateData) {
|
|
2601
|
+
return await this.decrypt(identifier, encryptedStateData);
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
async delete(identifier, options) {
|
|
2605
|
+
const cookieKeys = this.#getCookieKeys(identifier, options);
|
|
2606
|
+
for (const key of cookieKeys) {
|
|
2607
|
+
this.#cookieHandler.deleteCookie(key, options, this.#getCookieOptions());
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
/**
|
|
2611
|
+
* Absolute expiry of the anonymous session, in Unix seconds.
|
|
2612
|
+
*
|
|
2613
|
+
* Prefers the value Auth0 reports. `sessionTokenExpiresAt` is not populated yet, because
|
|
2614
|
+
* `auth0-auth-js` does not surface `session_expires_in` from the token response; until it
|
|
2615
|
+
* does, this falls back to the configured lifetime measured from creation.
|
|
2616
|
+
*/
|
|
2617
|
+
#getExpiration(anonymousStateData) {
|
|
2618
|
+
return anonymousStateData.sessionTokenExpiresAt ?? anonymousStateData.createdAt + this.#sessionTokenLifetime;
|
|
2619
|
+
}
|
|
2620
|
+
#getCookieKeys(identifier, options) {
|
|
2621
|
+
return Object.keys(this.#cookieHandler.getCookies(options)).filter((key) => key.startsWith(identifier));
|
|
2622
|
+
}
|
|
2623
|
+
#getCookieOptions(partial) {
|
|
2624
|
+
return {
|
|
2625
|
+
httpOnly: true,
|
|
2626
|
+
sameSite: this.#cookieOptions?.sameSite ?? "lax",
|
|
2627
|
+
path: this.#cookieOptions?.path ?? "/",
|
|
2628
|
+
secure: this.#cookieOptions?.secure ?? true,
|
|
2629
|
+
...partial
|
|
2630
|
+
};
|
|
2631
|
+
}
|
|
2632
|
+
};
|
|
2633
|
+
|
|
2172
2634
|
// src/mfa/index.ts
|
|
2173
2635
|
import {
|
|
2174
2636
|
MfaListAuthenticatorsError,
|
|
@@ -2188,12 +2650,19 @@ import {
|
|
|
2188
2650
|
|
|
2189
2651
|
// src/database/index.ts
|
|
2190
2652
|
import { SignUpError, ChangePasswordError } from "@auth0/auth0-auth-js";
|
|
2653
|
+
|
|
2654
|
+
// src/anonymous/index.ts
|
|
2655
|
+
import { AnonymousSessionError } from "@auth0/auth0-auth-js";
|
|
2191
2656
|
export {
|
|
2657
|
+
AbstractAnonymousStore,
|
|
2192
2658
|
AbstractStateStore,
|
|
2193
2659
|
AbstractTransactionStore,
|
|
2660
|
+
AnonymousSessionError,
|
|
2661
|
+
AnonymousSessionExpiredError,
|
|
2194
2662
|
BackchannelLogoutError,
|
|
2195
2663
|
ChangePasswordError,
|
|
2196
2664
|
CookieTransactionStore,
|
|
2665
|
+
DEFAULT_ANONYMOUS_SESSION_LIFETIME,
|
|
2197
2666
|
EnterpriseConnectNotSupportedError2 as EnterpriseConnectNotSupportedError,
|
|
2198
2667
|
InvalidConfigurationError,
|
|
2199
2668
|
IssuerValidationError,
|
|
@@ -2201,6 +2670,7 @@ export {
|
|
|
2201
2670
|
MfaEnrollmentError,
|
|
2202
2671
|
MfaListAuthenticatorsError,
|
|
2203
2672
|
MfaVerifyError,
|
|
2673
|
+
MissingAnonymousSessionError,
|
|
2204
2674
|
MissingClientAuthError,
|
|
2205
2675
|
MissingRequiredArgumentError,
|
|
2206
2676
|
MissingSessionError,
|
|
@@ -2211,6 +2681,7 @@ export {
|
|
|
2211
2681
|
PasskeyRegisterError,
|
|
2212
2682
|
PasswordlessStartError2 as PasswordlessStartError,
|
|
2213
2683
|
PasswordlessVerifyError2 as PasswordlessVerifyError,
|
|
2684
|
+
ServerAnonymousClient,
|
|
2214
2685
|
ServerClient,
|
|
2215
2686
|
ServerDatabaseClient,
|
|
2216
2687
|
ServerMfaClient,
|
|
@@ -2219,6 +2690,7 @@ export {
|
|
|
2219
2690
|
SignUpError,
|
|
2220
2691
|
StartLinkUserError,
|
|
2221
2692
|
StatefulStateStore,
|
|
2693
|
+
StatelessAnonymousStore,
|
|
2222
2694
|
StatelessStateStore,
|
|
2223
2695
|
TokenExchangeError2 as TokenExchangeError,
|
|
2224
2696
|
TokenExchangeErrorCode,
|