@auth0/auth0-server-js 1.15.0 → 1.16.1
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 +539 -51
- 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 +488 -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.1"
|
|
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,234 @@ function applyEnterpriseConnectRestrictions(instance) {
|
|
|
489
507
|
}
|
|
490
508
|
}
|
|
491
509
|
|
|
510
|
+
// src/anonymous/server-anonymous-client.ts
|
|
511
|
+
import { decodeJwt } from "jose";
|
|
512
|
+
import { AnonymousSessionError } from "@auth0/auth0-auth-js";
|
|
513
|
+
var DEFAULT_AUDIENCE_CACHE_KEY = "default";
|
|
514
|
+
var readAnonymousSub = (accessToken) => {
|
|
515
|
+
try {
|
|
516
|
+
const { sub } = decodeJwt(accessToken);
|
|
517
|
+
return typeof sub === "string" ? sub : void 0;
|
|
518
|
+
} catch {
|
|
519
|
+
return void 0;
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
var cacheKeyScope = (tokenSet) => tokenSet.requestedScope ?? tokenSet.scope;
|
|
523
|
+
var upsertTokenSet = (tokenSets, tokenSet) => {
|
|
524
|
+
const matches = (candidate) => candidate.audience === tokenSet.audience && cacheKeyScope(candidate) === cacheKeyScope(tokenSet);
|
|
525
|
+
return tokenSets.some(matches) ? tokenSets.map((candidate) => matches(candidate) ? tokenSet : candidate) : [...tokenSets, tokenSet];
|
|
526
|
+
};
|
|
527
|
+
var ServerAnonymousClient = class {
|
|
528
|
+
#options;
|
|
529
|
+
/**
|
|
530
|
+
* @internal
|
|
531
|
+
*/
|
|
532
|
+
constructor(options) {
|
|
533
|
+
this.#options = options;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Creates an anonymous session and stores it, returning the first anonymous access token.
|
|
537
|
+
*
|
|
538
|
+
* Replaces any anonymous session already stored for this visitor. Metadata can only be
|
|
539
|
+
* attached here, because Auth0 rejects a request that carries both metadata and an
|
|
540
|
+
* existing session.
|
|
541
|
+
*
|
|
542
|
+
* The anonymous identity (`sub`) is read off the first access token and stored alongside
|
|
543
|
+
* the metadata, so {@link ServerAnonymousClient.getSession} can hand both back without the
|
|
544
|
+
* application decoding a token. It stays `undefined` for an audience with token encryption
|
|
545
|
+
* (`token_encryption`) enabled, whose access token is an encrypted JWE only the API can
|
|
546
|
+
* read.
|
|
547
|
+
*
|
|
548
|
+
* @param options Optional audience, scope and metadata for the new session.
|
|
549
|
+
* @param storeOptions Optional options used to pass to the anonymous store (and to resolve the domain in resolver mode).
|
|
550
|
+
*
|
|
551
|
+
* @throws {AnonymousSessionError} If Auth0 rejected the request. Common codes are
|
|
552
|
+
* `feature_not_enabled` (the tenant flag is off), `unauthorized_client` (the client is
|
|
553
|
+
* not enabled for anonymous sessions), `invalid_target` (the resource server does not
|
|
554
|
+
* allow anonymous access), `invalid_request` (metadata over 1024 bytes, or not all
|
|
555
|
+
* strings) and `access_denied`. `code` is `server_error` when Auth0 answers with a body
|
|
556
|
+
* that is not JSON.
|
|
557
|
+
*
|
|
558
|
+
* Only call this once you have established the visitor has no anonymous session (check
|
|
559
|
+
* {@link ServerAnonymousClient.getSession} first), rather than on every request.
|
|
560
|
+
*
|
|
561
|
+
* @returns The anonymous access token for the requested audience.
|
|
562
|
+
*/
|
|
563
|
+
async createSession(options, storeOptions) {
|
|
564
|
+
const domain = await this.#options.resolveDomain(storeOptions);
|
|
565
|
+
const requestedAudience = options?.audience ?? this.#options.defaultAudience;
|
|
566
|
+
const audience = requestedAudience ?? DEFAULT_AUDIENCE_CACHE_KEY;
|
|
567
|
+
const scope = options?.scope;
|
|
568
|
+
const session = await this.#options.getAuthClient(domain).anonymous.createSession({
|
|
569
|
+
...requestedAudience && { audience: requestedAudience },
|
|
570
|
+
...scope && { scope },
|
|
571
|
+
...options?.metadata && { metadata: options.metadata }
|
|
572
|
+
});
|
|
573
|
+
const tokenSet = {
|
|
574
|
+
audience,
|
|
575
|
+
accessToken: session.accessToken,
|
|
576
|
+
scope: session.scope,
|
|
577
|
+
...scope && scope !== session.scope && { requestedScope: scope },
|
|
578
|
+
expiresAt: session.expiresAt
|
|
579
|
+
};
|
|
580
|
+
const stateData = {
|
|
581
|
+
sessionToken: session.sessionToken,
|
|
582
|
+
sessionTokenExpiresAt: session.sessionTokenExpiresAt,
|
|
583
|
+
sub: readAnonymousSub(session.accessToken),
|
|
584
|
+
...options?.metadata && { metadata: options.metadata },
|
|
585
|
+
createdAt: Math.floor(Date.now() / 1e3),
|
|
586
|
+
tokenSets: [tokenSet],
|
|
587
|
+
domain
|
|
588
|
+
};
|
|
589
|
+
await this.#options.anonymousStore.set(
|
|
590
|
+
this.#options.anonymousStoreIdentifier,
|
|
591
|
+
stateData,
|
|
592
|
+
true,
|
|
593
|
+
storeOptions
|
|
594
|
+
);
|
|
595
|
+
return tokenSet;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Returns an anonymous access token for the stored anonymous session, fetching a fresh
|
|
599
|
+
* one from Auth0 when the cached one has expired.
|
|
600
|
+
*
|
|
601
|
+
* Tokens are cached per audience and scope, so requesting a second audience returns a
|
|
602
|
+
* second token for the same anonymous identity without replacing the first.
|
|
603
|
+
*
|
|
604
|
+
* Auth0 may grant fewer scopes than requested — a scope the anonymous identity is not
|
|
605
|
+
* entitled to is silently dropped and the response is still a success. Always check
|
|
606
|
+
* `tokenSet.scope` before calling your API; do not assume the token carries every scope
|
|
607
|
+
* you asked for.
|
|
608
|
+
*
|
|
609
|
+
* This never creates a session. If the anonymous session has expired, the stored session
|
|
610
|
+
* is deleted and `AnonymousSessionExpiredError` is thrown, so a visitor is never moved
|
|
611
|
+
* onto a fresh anonymous identity behind your back.
|
|
612
|
+
*
|
|
613
|
+
* @param options Optional audience and scope for the requested token.
|
|
614
|
+
* @param storeOptions Optional options used to pass to the anonymous store (and to resolve the domain in resolver mode).
|
|
615
|
+
*
|
|
616
|
+
* @throws {MissingAnonymousSessionError} When there is no anonymous session stored, or the stored one belongs to another Auth0 domain (resolver mode).
|
|
617
|
+
* @throws {AnonymousSessionExpiredError} When the anonymous session has expired or Auth0 rejected the session token. The stored session is deleted first.
|
|
618
|
+
*
|
|
619
|
+
* @returns The anonymous access token for the requested audience.
|
|
620
|
+
*/
|
|
621
|
+
async getAccessToken(options, storeOptions) {
|
|
622
|
+
const stateData = await this.#options.anonymousStore.get(
|
|
623
|
+
this.#options.anonymousStoreIdentifier,
|
|
624
|
+
storeOptions
|
|
625
|
+
);
|
|
626
|
+
if (!stateData?.sessionToken) {
|
|
627
|
+
throw new MissingAnonymousSessionError();
|
|
628
|
+
}
|
|
629
|
+
const domain = await this.#options.resolveDomain(storeOptions);
|
|
630
|
+
if (this.#options.isResolverMode() && stateData.domain !== domain) {
|
|
631
|
+
throw new MissingAnonymousSessionError(
|
|
632
|
+
"The stored anonymous session was created for a different Auth0 domain."
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
const requestedAudience = options?.audience ?? this.#options.defaultAudience;
|
|
636
|
+
const audience = requestedAudience ?? DEFAULT_AUDIENCE_CACHE_KEY;
|
|
637
|
+
const scope = options?.scope;
|
|
638
|
+
const cachedTokenSet = stateData.tokenSets.find(
|
|
639
|
+
(tokenSet2) => tokenSet2.audience === audience && (!scope || cacheKeyScope(tokenSet2) === scope || compareScopes(tokenSet2.scope, scope))
|
|
640
|
+
);
|
|
641
|
+
if (cachedTokenSet && cachedTokenSet.expiresAt > Date.now() / 1e3) {
|
|
642
|
+
return cachedTokenSet;
|
|
643
|
+
}
|
|
644
|
+
let renewed;
|
|
645
|
+
try {
|
|
646
|
+
renewed = await this.#options.getAuthClient(domain).anonymous.getAccessToken({
|
|
647
|
+
sessionToken: stateData.sessionToken,
|
|
648
|
+
...requestedAudience && { audience: requestedAudience },
|
|
649
|
+
...scope && { scope }
|
|
650
|
+
});
|
|
651
|
+
} catch (e) {
|
|
652
|
+
if (e instanceof AnonymousSessionError && e.code === "session_expired") {
|
|
653
|
+
await this.#options.anonymousStore.delete(this.#options.anonymousStoreIdentifier, storeOptions);
|
|
654
|
+
throw new AnonymousSessionExpiredError();
|
|
655
|
+
}
|
|
656
|
+
if (e instanceof AnonymousSessionError && e.code === "invalid_session_token") {
|
|
657
|
+
await this.#options.anonymousStore.delete(this.#options.anonymousStoreIdentifier, storeOptions);
|
|
658
|
+
}
|
|
659
|
+
throw e;
|
|
660
|
+
}
|
|
661
|
+
const tokenSet = {
|
|
662
|
+
audience,
|
|
663
|
+
accessToken: renewed.accessToken,
|
|
664
|
+
scope: renewed.scope,
|
|
665
|
+
...scope && scope !== renewed.scope && { requestedScope: scope },
|
|
666
|
+
expiresAt: renewed.expiresAt
|
|
667
|
+
};
|
|
668
|
+
const existingStateData = await this.#options.anonymousStore.get(
|
|
669
|
+
this.#options.anonymousStoreIdentifier,
|
|
670
|
+
storeOptions
|
|
671
|
+
);
|
|
672
|
+
if (existingStateData?.sessionToken === stateData.sessionToken) {
|
|
673
|
+
await this.#options.anonymousStore.set(
|
|
674
|
+
this.#options.anonymousStoreIdentifier,
|
|
675
|
+
{
|
|
676
|
+
...existingStateData,
|
|
677
|
+
...existingStateData.sub === void 0 && { sub: readAnonymousSub(renewed.accessToken) },
|
|
678
|
+
tokenSets: upsertTokenSet(existingStateData.tokenSets, tokenSet)
|
|
679
|
+
},
|
|
680
|
+
false,
|
|
681
|
+
storeOptions
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
return tokenSet;
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Returns the stored anonymous session, or `undefined` when there is none.
|
|
688
|
+
*
|
|
689
|
+
* Two common uses:
|
|
690
|
+
* - **Gate `createSession`** — call this first; only create a session when the result is `undefined`.
|
|
691
|
+
* - **Read identity for a merge** — `sub` and `metadata` are available here without decoding a token.
|
|
692
|
+
*
|
|
693
|
+
* This is a local read of the store with no request to Auth0. It cannot tell you whether
|
|
694
|
+
* Auth0 still considers the session valid — only {@link ServerAnonymousClient.getAccessToken} can.
|
|
695
|
+
* The session token is never included in the result.
|
|
696
|
+
*
|
|
697
|
+
* @param storeOptions Optional options used to pass to the anonymous store (and to resolve the domain in resolver mode).
|
|
698
|
+
*
|
|
699
|
+
* @returns The anonymous session, or `undefined` when there is none for this visitor (or it belongs to another Auth0 domain in resolver mode).
|
|
700
|
+
*/
|
|
701
|
+
async getSession(storeOptions) {
|
|
702
|
+
const stateData = await this.#options.anonymousStore.get(
|
|
703
|
+
this.#options.anonymousStoreIdentifier,
|
|
704
|
+
storeOptions
|
|
705
|
+
);
|
|
706
|
+
if (!stateData) {
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
if (this.#options.isResolverMode()) {
|
|
710
|
+
const domain = await this.#options.resolveDomain(storeOptions);
|
|
711
|
+
if (stateData.domain !== domain) {
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
const { sessionToken, ...sessionData } = stateData;
|
|
716
|
+
return sessionData;
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Clears the anonymous session from the store.
|
|
720
|
+
*
|
|
721
|
+
* Does not call `POST /anonymous/logout` — that endpoint only clears the `auth0_anon`
|
|
722
|
+
* browser cookie, which your server never holds. Any access tokens already issued remain
|
|
723
|
+
* valid until they expire (~2 hours by default).
|
|
724
|
+
*
|
|
725
|
+
* You rarely need to call this directly: every method that establishes a user session
|
|
726
|
+
* clears the anonymous session automatically (unless `clearAnonymousSessionOnLogin: false`
|
|
727
|
+
* is set), and `serverClient.logout()` clears it too. Call this to explicitly reset a
|
|
728
|
+
* visitor's anonymous identity, or to clean up after your own post-login merge logic when
|
|
729
|
+
* automatic clearing is disabled.
|
|
730
|
+
*
|
|
731
|
+
* @param storeOptions Optional options used to pass to the anonymous store.
|
|
732
|
+
*/
|
|
733
|
+
async logout(storeOptions) {
|
|
734
|
+
await this.#options.anonymousStore.delete(this.#options.anonymousStoreIdentifier, storeOptions);
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
|
|
492
738
|
// src/server-client.ts
|
|
493
739
|
var normalizeDomain = (value) => {
|
|
494
740
|
const trimmed = value.trim();
|
|
@@ -497,7 +743,7 @@ var normalizeDomain = (value) => {
|
|
|
497
743
|
};
|
|
498
744
|
var decodeIssuer = (token) => {
|
|
499
745
|
try {
|
|
500
|
-
const { iss } =
|
|
746
|
+
const { iss } = decodeJwt2(token);
|
|
501
747
|
return typeof iss === "string" ? iss : void 0;
|
|
502
748
|
} catch {
|
|
503
749
|
return void 0;
|
|
@@ -512,7 +758,7 @@ var actorUnavailableError = (message) => {
|
|
|
512
758
|
};
|
|
513
759
|
var isTokenExpired = (token) => {
|
|
514
760
|
try {
|
|
515
|
-
const { exp } =
|
|
761
|
+
const { exp } = decodeJwt2(token);
|
|
516
762
|
if (typeof exp !== "number") {
|
|
517
763
|
return true;
|
|
518
764
|
}
|
|
@@ -534,6 +780,7 @@ var ServerClient = class {
|
|
|
534
780
|
#mfaClient;
|
|
535
781
|
#passkeyClient;
|
|
536
782
|
#databaseClient;
|
|
783
|
+
#anonymousClient;
|
|
537
784
|
/**
|
|
538
785
|
* The underlying `authClient` instance that can be used to interact with the Auth0 Authentication API.
|
|
539
786
|
* Generally, you should prefer to use the higher-level methods exposed on the `ServerClient` instance.
|
|
@@ -596,6 +843,42 @@ var ServerClient = class {
|
|
|
596
843
|
get database() {
|
|
597
844
|
return this.#databaseClient;
|
|
598
845
|
}
|
|
846
|
+
/**
|
|
847
|
+
* The anonymous session client, for giving a visitor who has not logged in a stable
|
|
848
|
+
* identity and an access token for your API.
|
|
849
|
+
*
|
|
850
|
+
* Provides `createSession()` to establish the anonymous identity, `getAccessToken()` to
|
|
851
|
+
* obtain and renew anonymous access tokens, `getSession()` to check whether a visitor
|
|
852
|
+
* already has one, and `logout()` to discard it.
|
|
853
|
+
*
|
|
854
|
+
* Requires `anonymousStore` on the `ServerClient`, and a tenant and client configured for
|
|
855
|
+
* anonymous sessions. Anonymous sessions are kept in that store, never in the state
|
|
856
|
+
* store, so `getSession()` and `getUser()` still report no user while one is active.
|
|
857
|
+
*
|
|
858
|
+
* The anonymous session ends when the visitor logs in: every login method clears it once
|
|
859
|
+
* the user session is written, unless you set `clearAnonymousSessionOnLogin: false`.
|
|
860
|
+
* {@link ServerClient.logout} clears it too.
|
|
861
|
+
*
|
|
862
|
+
* For `/authorize` flows, `startInteractiveLogin()` links the anonymous session to
|
|
863
|
+
* the user automatically via a Session Transfer Ticket — no extra configuration needed.
|
|
864
|
+
* For logins that bypass `/authorize` (passkey, passwordless, backchannel, custom token
|
|
865
|
+
* exchange), the anonymous session is not linked. Read `anonymous.getSession()` before
|
|
866
|
+
* the login to get the anonymous `sub` for a manual merge, or set
|
|
867
|
+
* `clearAnonymousSessionOnLogin: false` and clear it yourself afterwards.
|
|
868
|
+
*
|
|
869
|
+
* Like `passkey` and `database`, this works in both static and resolver (multi-tenant)
|
|
870
|
+
* domain modes.
|
|
871
|
+
*
|
|
872
|
+
* @throws {InvalidConfigurationError} When no `anonymousStore` is configured.
|
|
873
|
+
*/
|
|
874
|
+
get anonymous() {
|
|
875
|
+
if (!this.#anonymousClient) {
|
|
876
|
+
throw new InvalidConfigurationError(
|
|
877
|
+
"anonymous is only available when an `anonymousStore` is configured on the ServerClient."
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
return this.#anonymousClient;
|
|
881
|
+
}
|
|
599
882
|
constructor(options) {
|
|
600
883
|
this.#options = options;
|
|
601
884
|
this.#enterpriseConnect = !!options.enterpriseConnect;
|
|
@@ -654,7 +937,8 @@ var ServerClient = class {
|
|
|
654
937
|
domain,
|
|
655
938
|
stateStore: this.#stateStore,
|
|
656
939
|
stateStoreIdentifier: this.#stateStoreIdentifier,
|
|
657
|
-
defaultAudience: this.#options.authorizationParams?.audience ?? "default"
|
|
940
|
+
defaultAudience: this.#options.authorizationParams?.audience ?? "default",
|
|
941
|
+
onUserSessionEstablished: (storeOptions) => this.#clearAnonymousSessionAfterLogin(storeOptions)
|
|
658
942
|
});
|
|
659
943
|
}
|
|
660
944
|
this.#passkeyClient = new ServerPasskeyClient({
|
|
@@ -663,12 +947,26 @@ var ServerClient = class {
|
|
|
663
947
|
stateStore: this.#stateStore,
|
|
664
948
|
stateStoreIdentifier: this.#stateStoreIdentifier,
|
|
665
949
|
defaultScope: this.#options.authorizationParams?.scope,
|
|
666
|
-
defaultAudience: this.#options.authorizationParams?.audience
|
|
950
|
+
defaultAudience: this.#options.authorizationParams?.audience,
|
|
951
|
+
onUserSessionEstablished: (storeOptions) => this.#clearAnonymousSessionAfterLogin(storeOptions)
|
|
667
952
|
});
|
|
668
953
|
this.#databaseClient = new ServerDatabaseClient({
|
|
669
954
|
resolveDomain: (storeOptions) => this.#resolveDomain(storeOptions),
|
|
670
955
|
getAuthClient: (domain) => this.#getAuthClient(domain)
|
|
671
956
|
});
|
|
957
|
+
if (this.#options.anonymousStore) {
|
|
958
|
+
this.#anonymousClient = new ServerAnonymousClient({
|
|
959
|
+
resolveDomain: (storeOptions) => this.#resolveDomain(storeOptions),
|
|
960
|
+
getAuthClient: (domain) => this.#getAuthClient(domain),
|
|
961
|
+
isResolverMode: () => this.#isResolverMode(),
|
|
962
|
+
anonymousStore: this.#options.anonymousStore,
|
|
963
|
+
anonymousStoreIdentifier: this.#options.anonymousSessionIdentifier || "__a0_anon",
|
|
964
|
+
// Only the audience is inherited from the client-level authorizationParams. The
|
|
965
|
+
// scope there is written for a user session (`openid profile email offline_access`)
|
|
966
|
+
// and none of it applies to an anonymous identity.
|
|
967
|
+
defaultAudience: this.#options.authorizationParams?.audience
|
|
968
|
+
});
|
|
969
|
+
}
|
|
672
970
|
}
|
|
673
971
|
async #resolveDomain(storeOptions) {
|
|
674
972
|
if (typeof this.#options.domain === "function") {
|
|
@@ -745,6 +1043,54 @@ var ServerClient = class {
|
|
|
745
1043
|
storeOptions
|
|
746
1044
|
);
|
|
747
1045
|
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Discards the anonymous session once a login has established a user session.
|
|
1048
|
+
*
|
|
1049
|
+
* Called by every method that writes a user session, always AFTER the state store has been
|
|
1050
|
+
* written. An anonymous session outliving the login would leave a 30-day bearer credential
|
|
1051
|
+
* in a cookie on every request, and `anonymous.getAccessToken()` would keep minting
|
|
1052
|
+
* anonymous tokens for a visitor who is no longer anonymous.
|
|
1053
|
+
*
|
|
1054
|
+
* Two deliberate properties:
|
|
1055
|
+
* - **Best-effort.** The user is authenticated and their session is already persisted by
|
|
1056
|
+
* this point, so a failing anonymous store must not turn a successful login into an
|
|
1057
|
+
* error. The visitor would be unable to log in at all because of leftover state they
|
|
1058
|
+
* cannot see or clear.
|
|
1059
|
+
* - **Unconditional across domains.** In resolver (multi-tenant) mode the anonymous session
|
|
1060
|
+
* is dropped even when it was created against a different Auth0 domain than the one just
|
|
1061
|
+
* logged into. The anonymous store holds a single session per visitor, and "no anonymous
|
|
1062
|
+
* session survives a login" is a rule that can be reasoned about; a domain-dependent one
|
|
1063
|
+
* cannot.
|
|
1064
|
+
*
|
|
1065
|
+
* Skipped entirely when no `anonymousStore` is configured, or when the application opted
|
|
1066
|
+
* out with `clearAnonymousSessionOnLogin: false`.
|
|
1067
|
+
*/
|
|
1068
|
+
async #clearAnonymousSessionAfterLogin(storeOptions) {
|
|
1069
|
+
if (this.#options.clearAnonymousSessionOnLogin === false) {
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
await this.#discardAnonymousSession(storeOptions);
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Drops the anonymous session, swallowing store failures.
|
|
1076
|
+
*
|
|
1077
|
+
* Shared by the login sites and {@link ServerClient.logout}. Both run after the user
|
|
1078
|
+
* session has already been written or deleted, so the outcome the caller cares about is
|
|
1079
|
+
* settled: a failing anonymous store must not fail a login the user completed, nor block a
|
|
1080
|
+
* logout redirect and leave the visitor unable to log out. When the store is the
|
|
1081
|
+
* cookie-backed default this cannot fail, since deleting is a `Set-Cookie` on the response.
|
|
1082
|
+
*
|
|
1083
|
+
* No-op when no `anonymousStore` is configured.
|
|
1084
|
+
*/
|
|
1085
|
+
async #discardAnonymousSession(storeOptions) {
|
|
1086
|
+
if (!this.#anonymousClient) {
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
try {
|
|
1090
|
+
await this.#anonymousClient.logout(storeOptions);
|
|
1091
|
+
} catch {
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
748
1094
|
/**
|
|
749
1095
|
* Starts the interactive login process, and returns a URL to redirect the user-agent to to request authorization at Auth0.
|
|
750
1096
|
*
|
|
@@ -780,6 +1126,17 @@ var ServerClient = class {
|
|
|
780
1126
|
}
|
|
781
1127
|
const domain = await this.#resolveDomain(storeOptions);
|
|
782
1128
|
const authClient = this.#getAuthClient(domain);
|
|
1129
|
+
let anonTransferToken;
|
|
1130
|
+
if (this.#options.anonymousStore) {
|
|
1131
|
+
try {
|
|
1132
|
+
const identifier = this.#options.anonymousSessionIdentifier || "__a0_anon";
|
|
1133
|
+
const stateData = await this.#options.anonymousStore.get(identifier, storeOptions);
|
|
1134
|
+
if (stateData?.sessionToken) {
|
|
1135
|
+
anonTransferToken = await authClient.anonymous.mintTransferToken(stateData.sessionToken) ?? void 0;
|
|
1136
|
+
}
|
|
1137
|
+
} catch {
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
783
1140
|
const { codeVerifier, authorizationUrl } = await authClient.buildAuthorizationUrl({
|
|
784
1141
|
pushedAuthorizationRequests: options?.pushedAuthorizationRequests,
|
|
785
1142
|
authorizationParams: {
|
|
@@ -787,7 +1144,8 @@ var ServerClient = class {
|
|
|
787
1144
|
redirect_uri: redirectUri,
|
|
788
1145
|
scope,
|
|
789
1146
|
...resolvedOrganization ? { organization: resolvedOrganization } : {},
|
|
790
|
-
...options?.invitation ? { invitation: options.invitation } : {}
|
|
1147
|
+
...options?.invitation ? { invitation: options.invitation } : {},
|
|
1148
|
+
...anonTransferToken ? { anon_transfer_token: anonTransferToken } : {}
|
|
791
1149
|
}
|
|
792
1150
|
});
|
|
793
1151
|
const transactionState = {
|
|
@@ -857,6 +1215,7 @@ var ServerClient = class {
|
|
|
857
1215
|
tokenEndpointResponse.claims
|
|
858
1216
|
);
|
|
859
1217
|
await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
|
|
1218
|
+
await this.#clearAnonymousSessionAfterLogin(storeOptions);
|
|
860
1219
|
return {
|
|
861
1220
|
appState: transactionData.appState,
|
|
862
1221
|
authorizationDetails: tokenEndpointResponse.authorizationDetails
|
|
@@ -1024,6 +1383,7 @@ var ServerClient = class {
|
|
|
1024
1383
|
tokenEndpointResponse.claims
|
|
1025
1384
|
);
|
|
1026
1385
|
await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
|
|
1386
|
+
await this.#clearAnonymousSessionAfterLogin(storeOptions);
|
|
1027
1387
|
const result = {
|
|
1028
1388
|
authorizationDetails: tokenEndpointResponse.authorizationDetails
|
|
1029
1389
|
};
|
|
@@ -1170,6 +1530,7 @@ var ServerClient = class {
|
|
|
1170
1530
|
{ domain }
|
|
1171
1531
|
);
|
|
1172
1532
|
await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
|
|
1533
|
+
await this.#clearAnonymousSessionAfterLogin(storeOptions);
|
|
1173
1534
|
const result = {
|
|
1174
1535
|
authorizationDetails: tokenEndpointResponse.authorizationDetails
|
|
1175
1536
|
};
|
|
@@ -1220,6 +1581,7 @@ var ServerClient = class {
|
|
|
1220
1581
|
domain
|
|
1221
1582
|
});
|
|
1222
1583
|
await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
|
|
1584
|
+
await this.#clearAnonymousSessionAfterLogin(storeOptions);
|
|
1223
1585
|
await this.#transactionStore.delete(this.#transactionStoreIdentifier, storeOptions);
|
|
1224
1586
|
return {
|
|
1225
1587
|
authorizationDetails: tokenEndpointResponse.authorizationDetails
|
|
@@ -1527,6 +1889,29 @@ var ServerClient = class {
|
|
|
1527
1889
|
}
|
|
1528
1890
|
/**
|
|
1529
1891
|
* Logs the user out and returns a URL to redirect the user-agent to after they log out.
|
|
1892
|
+
*
|
|
1893
|
+
* Clears the anonymous session as well, whenever an `anonymousStore` is configured. Logging
|
|
1894
|
+
* out means the visitor is done, so leaving a 30-day anonymous credential behind in a
|
|
1895
|
+
* cookie would be surprising, and `anonymous.getAccessToken()` would keep working right
|
|
1896
|
+
* after the user logged out. This happens even when there is no user session to clear: a
|
|
1897
|
+
* visitor who only ever had an anonymous session can log out. Unlike the automatic clearing
|
|
1898
|
+
* at login, it is not affected by `clearAnonymousSessionOnLogin`, which exists so an
|
|
1899
|
+
* application can finish its post-login work.
|
|
1900
|
+
*
|
|
1901
|
+
* In resolver (multi-tenant) mode the one exception is a stored user session belonging to a
|
|
1902
|
+
* different Auth0 domain than the request resolves to. Nothing local is cleared in that
|
|
1903
|
+
* case, anonymous session included, exactly as today: that state belongs to another tenant.
|
|
1904
|
+
*
|
|
1905
|
+
* The anonymous session is only ever cleared locally. The SDK does not call
|
|
1906
|
+
* `POST /anonymous/logout`: that endpoint exists to clear the `auth0_anon` cookie in a
|
|
1907
|
+
* browser, and it revokes nothing server-side. Your server never holds that cookie, so
|
|
1908
|
+
* calling it would achieve nothing. If the visitor's browser created an anonymous session
|
|
1909
|
+
* of its own (through `@auth0/auth0-spa-js`, for example), that cookie is not covered by
|
|
1910
|
+
* this SDK and has to be cleared from the browser.
|
|
1911
|
+
*
|
|
1912
|
+
* Anonymous access tokens already handed out stay valid until they expire; there is no
|
|
1913
|
+
* anonymous session to revoke them against.
|
|
1914
|
+
*
|
|
1530
1915
|
* @param options Options used to configure the logout process.
|
|
1531
1916
|
* @param storeOptions Optional options used to pass to the Transaction and State Store.
|
|
1532
1917
|
* @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 +1932,14 @@ var ServerClient = class {
|
|
|
1547
1932
|
} catch {
|
|
1548
1933
|
}
|
|
1549
1934
|
await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
|
|
1935
|
+
await this.#discardAnonymousSession(storeOptions);
|
|
1550
1936
|
return this.authClient.buildLogoutUrl(options);
|
|
1551
1937
|
}
|
|
1552
1938
|
const resolvedDomain = await this.#resolveDomain(storeOptions);
|
|
1553
1939
|
const authClient = this.#getAuthClient(resolvedDomain);
|
|
1554
1940
|
const stateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
|
|
1555
1941
|
if (!stateData) {
|
|
1942
|
+
await this.#discardAnonymousSession(storeOptions);
|
|
1556
1943
|
return authClient.buildLogoutUrl(options);
|
|
1557
1944
|
}
|
|
1558
1945
|
const sessionDomain = this.#getSessionDomain(stateData);
|
|
@@ -1563,6 +1950,7 @@ var ServerClient = class {
|
|
|
1563
1950
|
} catch {
|
|
1564
1951
|
}
|
|
1565
1952
|
await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
|
|
1953
|
+
await this.#discardAnonymousSession(storeOptions);
|
|
1566
1954
|
}
|
|
1567
1955
|
return authClient.buildLogoutUrl(options);
|
|
1568
1956
|
}
|
|
@@ -1594,6 +1982,7 @@ var ServerClient = class {
|
|
|
1594
1982
|
{ domain }
|
|
1595
1983
|
);
|
|
1596
1984
|
await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
|
|
1985
|
+
await this.#clearAnonymousSessionAfterLogin(storeOptions);
|
|
1597
1986
|
const result = {
|
|
1598
1987
|
authorizationDetails: tokenEndpointResponse.authorizationDetails
|
|
1599
1988
|
};
|
|
@@ -2169,6 +2558,88 @@ var StatelessStateStore = class extends AbstractSessionStore {
|
|
|
2169
2558
|
}
|
|
2170
2559
|
};
|
|
2171
2560
|
|
|
2561
|
+
// src/store/abstract-anonymous-store.ts
|
|
2562
|
+
var AbstractAnonymousStore = class extends AbstractStore {
|
|
2563
|
+
constructor(options) {
|
|
2564
|
+
super(options);
|
|
2565
|
+
}
|
|
2566
|
+
};
|
|
2567
|
+
|
|
2568
|
+
// src/store/stateless-anonymous-store.ts
|
|
2569
|
+
var DEFAULT_ANONYMOUS_SESSION_LIFETIME = 60 * 60 * 24 * 30;
|
|
2570
|
+
var CHUNK_SIZE = 3072;
|
|
2571
|
+
var StatelessAnonymousStore = class extends AbstractAnonymousStore {
|
|
2572
|
+
#cookieOptions;
|
|
2573
|
+
#cookieHandler;
|
|
2574
|
+
#sessionTokenLifetime;
|
|
2575
|
+
constructor(options, cookieHandler) {
|
|
2576
|
+
super(options);
|
|
2577
|
+
this.#cookieOptions = options.cookie;
|
|
2578
|
+
this.#cookieHandler = cookieHandler;
|
|
2579
|
+
this.#sessionTokenLifetime = options.sessionTokenLifetime ?? DEFAULT_ANONYMOUS_SESSION_LIFETIME;
|
|
2580
|
+
}
|
|
2581
|
+
async set(identifier, anonymousStateData, removeIfExists, options) {
|
|
2582
|
+
const expiration = this.#getExpiration(anonymousStateData);
|
|
2583
|
+
const maxAge = expiration - Math.floor(Date.now() / 1e3);
|
|
2584
|
+
if (maxAge <= 0) {
|
|
2585
|
+
await this.delete(identifier, options);
|
|
2586
|
+
return;
|
|
2587
|
+
}
|
|
2588
|
+
const cookieOpts = this.#getCookieOptions({ maxAge });
|
|
2589
|
+
const encryptedStateData = await this.encrypt(identifier, anonymousStateData, expiration);
|
|
2590
|
+
const chunkCount = Math.ceil(encryptedStateData.length / CHUNK_SIZE);
|
|
2591
|
+
const chunks = [...Array(chunkCount).keys()].map((i) => ({
|
|
2592
|
+
value: encryptedStateData.substring(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE),
|
|
2593
|
+
name: `${identifier}.${i}`
|
|
2594
|
+
}));
|
|
2595
|
+
chunks.forEach((chunk) => {
|
|
2596
|
+
this.#cookieHandler.setCookie(chunk.name, chunk.value, cookieOpts, options);
|
|
2597
|
+
});
|
|
2598
|
+
const existingCookieKeys = this.#getCookieKeys(identifier, options);
|
|
2599
|
+
existingCookieKeys.filter((key) => !chunks.some((chunk) => chunk.name === key)).forEach((key) => {
|
|
2600
|
+
this.#cookieHandler.deleteCookie(key, options, this.#getCookieOptions());
|
|
2601
|
+
});
|
|
2602
|
+
}
|
|
2603
|
+
async get(identifier, options) {
|
|
2604
|
+
const cookieKeys = this.#getCookieKeys(identifier, options);
|
|
2605
|
+
const encryptedStateData = cookieKeys.map((key) => ({
|
|
2606
|
+
index: parseInt(key.split(".")[1], 10),
|
|
2607
|
+
value: this.#cookieHandler.getCookie(key, options)
|
|
2608
|
+
})).sort((a, b) => a.index - b.index).map((item) => item.value).join("");
|
|
2609
|
+
if (encryptedStateData) {
|
|
2610
|
+
return await this.decrypt(identifier, encryptedStateData);
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
async delete(identifier, options) {
|
|
2614
|
+
const cookieKeys = this.#getCookieKeys(identifier, options);
|
|
2615
|
+
for (const key of cookieKeys) {
|
|
2616
|
+
this.#cookieHandler.deleteCookie(key, options, this.#getCookieOptions());
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
/**
|
|
2620
|
+
* Absolute expiry of the anonymous session, in Unix seconds.
|
|
2621
|
+
*
|
|
2622
|
+
* Prefers the value Auth0 reports. `sessionTokenExpiresAt` is not populated yet, because
|
|
2623
|
+
* `auth0-auth-js` does not surface `session_expires_in` from the token response; until it
|
|
2624
|
+
* does, this falls back to the configured lifetime measured from creation.
|
|
2625
|
+
*/
|
|
2626
|
+
#getExpiration(anonymousStateData) {
|
|
2627
|
+
return anonymousStateData.sessionTokenExpiresAt ?? anonymousStateData.createdAt + this.#sessionTokenLifetime;
|
|
2628
|
+
}
|
|
2629
|
+
#getCookieKeys(identifier, options) {
|
|
2630
|
+
return Object.keys(this.#cookieHandler.getCookies(options)).filter((key) => key.startsWith(identifier));
|
|
2631
|
+
}
|
|
2632
|
+
#getCookieOptions(partial) {
|
|
2633
|
+
return {
|
|
2634
|
+
httpOnly: true,
|
|
2635
|
+
sameSite: this.#cookieOptions?.sameSite ?? "lax",
|
|
2636
|
+
path: this.#cookieOptions?.path ?? "/",
|
|
2637
|
+
secure: this.#cookieOptions?.secure ?? true,
|
|
2638
|
+
...partial
|
|
2639
|
+
};
|
|
2640
|
+
}
|
|
2641
|
+
};
|
|
2642
|
+
|
|
2172
2643
|
// src/mfa/index.ts
|
|
2173
2644
|
import {
|
|
2174
2645
|
MfaListAuthenticatorsError,
|
|
@@ -2188,12 +2659,19 @@ import {
|
|
|
2188
2659
|
|
|
2189
2660
|
// src/database/index.ts
|
|
2190
2661
|
import { SignUpError, ChangePasswordError } from "@auth0/auth0-auth-js";
|
|
2662
|
+
|
|
2663
|
+
// src/anonymous/index.ts
|
|
2664
|
+
import { AnonymousSessionError as AnonymousSessionError2 } from "@auth0/auth0-auth-js";
|
|
2191
2665
|
export {
|
|
2666
|
+
AbstractAnonymousStore,
|
|
2192
2667
|
AbstractStateStore,
|
|
2193
2668
|
AbstractTransactionStore,
|
|
2669
|
+
AnonymousSessionError2 as AnonymousSessionError,
|
|
2670
|
+
AnonymousSessionExpiredError,
|
|
2194
2671
|
BackchannelLogoutError,
|
|
2195
2672
|
ChangePasswordError,
|
|
2196
2673
|
CookieTransactionStore,
|
|
2674
|
+
DEFAULT_ANONYMOUS_SESSION_LIFETIME,
|
|
2197
2675
|
EnterpriseConnectNotSupportedError2 as EnterpriseConnectNotSupportedError,
|
|
2198
2676
|
InvalidConfigurationError,
|
|
2199
2677
|
IssuerValidationError,
|
|
@@ -2201,6 +2679,7 @@ export {
|
|
|
2201
2679
|
MfaEnrollmentError,
|
|
2202
2680
|
MfaListAuthenticatorsError,
|
|
2203
2681
|
MfaVerifyError,
|
|
2682
|
+
MissingAnonymousSessionError,
|
|
2204
2683
|
MissingClientAuthError,
|
|
2205
2684
|
MissingRequiredArgumentError,
|
|
2206
2685
|
MissingSessionError,
|
|
@@ -2211,6 +2690,7 @@ export {
|
|
|
2211
2690
|
PasskeyRegisterError,
|
|
2212
2691
|
PasswordlessStartError2 as PasswordlessStartError,
|
|
2213
2692
|
PasswordlessVerifyError2 as PasswordlessVerifyError,
|
|
2693
|
+
ServerAnonymousClient,
|
|
2214
2694
|
ServerClient,
|
|
2215
2695
|
ServerDatabaseClient,
|
|
2216
2696
|
ServerMfaClient,
|
|
@@ -2219,6 +2699,7 @@ export {
|
|
|
2219
2699
|
SignUpError,
|
|
2220
2700
|
StartLinkUserError,
|
|
2221
2701
|
StatefulStateStore,
|
|
2702
|
+
StatelessAnonymousStore,
|
|
2222
2703
|
StatelessStateStore,
|
|
2223
2704
|
TokenExchangeError2 as TokenExchangeError,
|
|
2224
2705
|
TokenExchangeErrorCode,
|