@auth0/auth0-server-js 1.6.1 → 1.8.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.cjs +435 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +327 -14
- package/dist/index.d.ts +327 -14
- package/dist/index.js +433 -37
- package/dist/index.js.map +1 -1
- package/package.json +14 -14
package/dist/index.cjs
CHANGED
|
@@ -30,16 +30,22 @@ __export(index_exports, {
|
|
|
30
30
|
MfaEnrollmentError: () => import_auth0_auth_js2.MfaEnrollmentError,
|
|
31
31
|
MfaListAuthenticatorsError: () => import_auth0_auth_js2.MfaListAuthenticatorsError,
|
|
32
32
|
MfaVerifyError: () => import_auth0_auth_js2.MfaVerifyError,
|
|
33
|
-
MissingClientAuthError: () =>
|
|
33
|
+
MissingClientAuthError: () => import_auth0_auth_js4.MissingClientAuthError,
|
|
34
34
|
MissingRequiredArgumentError: () => MissingRequiredArgumentError,
|
|
35
35
|
MissingSessionError: () => MissingSessionError,
|
|
36
36
|
MissingTransactionError: () => MissingTransactionError,
|
|
37
|
+
OrganizationValidationError: () => import_auth0_auth_js3.OrganizationValidationError,
|
|
38
|
+
PasskeyChallengeError: () => import_auth0_auth_js3.PasskeyChallengeError,
|
|
39
|
+
PasskeyGetTokenError: () => import_auth0_auth_js3.PasskeyGetTokenError,
|
|
40
|
+
PasskeyRegisterError: () => import_auth0_auth_js3.PasskeyRegisterError,
|
|
37
41
|
ServerClient: () => ServerClient,
|
|
38
42
|
ServerMfaClient: () => ServerMfaClient,
|
|
43
|
+
ServerPasskeyClient: () => ServerPasskeyClient,
|
|
44
|
+
SessionExpiredError: () => SessionExpiredError,
|
|
39
45
|
StartLinkUserError: () => StartLinkUserError,
|
|
40
46
|
StatefulStateStore: () => StatefulStateStore,
|
|
41
47
|
StatelessStateStore: () => StatelessStateStore,
|
|
42
|
-
TokenExchangeError: () =>
|
|
48
|
+
TokenExchangeError: () => import_auth0_auth_js4.TokenExchangeError,
|
|
43
49
|
isMfaRequiredError: () => import_auth0_auth_js2.isMfaRequiredError
|
|
44
50
|
});
|
|
45
51
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -94,14 +100,55 @@ var IssuerValidationError = class extends Error {
|
|
|
94
100
|
this.name = "IssuerValidationError";
|
|
95
101
|
}
|
|
96
102
|
};
|
|
103
|
+
var SessionExpiredError = class extends Error {
|
|
104
|
+
code = "session_expired";
|
|
105
|
+
constructor(message) {
|
|
106
|
+
super(
|
|
107
|
+
message ?? "The session has expired because the upstream identity provider session ceiling was reached. The user needs to re-authenticate."
|
|
108
|
+
);
|
|
109
|
+
this.name = "SessionExpiredError";
|
|
110
|
+
}
|
|
111
|
+
};
|
|
97
112
|
|
|
98
113
|
// src/state/utils.ts
|
|
114
|
+
var SESSION_EXPIRY_LEEWAY = 30;
|
|
115
|
+
var MAX_PLAUSIBLE_UNIX_SECONDS = 1e10;
|
|
116
|
+
function isPlausibleUnixSeconds(value) {
|
|
117
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 && value < MAX_PLAUSIBLE_UNIX_SECONDS;
|
|
118
|
+
}
|
|
119
|
+
function extractSessionExpiry(claims) {
|
|
120
|
+
const value = claims?.session_expiry;
|
|
121
|
+
return isPlausibleUnixSeconds(value) ? value : void 0;
|
|
122
|
+
}
|
|
123
|
+
function isSessionExpiryReached(sessionExpiresAt, nowSeconds) {
|
|
124
|
+
if (sessionExpiresAt === void 0) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
const now = nowSeconds ?? Math.floor(Date.now() / 1e3);
|
|
128
|
+
return now >= sessionExpiresAt - SESSION_EXPIRY_LEEWAY;
|
|
129
|
+
}
|
|
99
130
|
var createUpdatedTokenSet = (audience, response) => ({
|
|
100
131
|
audience,
|
|
101
132
|
accessToken: response.accessToken,
|
|
102
133
|
scope: response.scope,
|
|
103
134
|
expiresAt: response.expiresAt
|
|
104
135
|
});
|
|
136
|
+
function isSessionExpiryInPast(sessionExpiresAt, issuedAt) {
|
|
137
|
+
if (sessionExpiresAt === void 0) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
const reference = isPlausibleUnixSeconds(issuedAt) ? issuedAt : Math.floor(Date.now() / 1e3);
|
|
141
|
+
return sessionExpiresAt <= reference + SESSION_EXPIRY_LEEWAY;
|
|
142
|
+
}
|
|
143
|
+
function applySessionExpiryAtLogin(stateData, claims) {
|
|
144
|
+
const sessionExpiresAt = extractSessionExpiry(claims);
|
|
145
|
+
if (isSessionExpiryInPast(sessionExpiresAt, claims?.iat)) {
|
|
146
|
+
throw new SessionExpiredError(
|
|
147
|
+
"The upstream identity provider session_expiry is at or before the issued-at time; refusing to create an already-expired session."
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return { ...stateData, sessionExpiresAt };
|
|
151
|
+
}
|
|
105
152
|
function updateStateData(audience, stateData, tokenEndpointResponse, context) {
|
|
106
153
|
if (stateData && tokenEndpointResponse.claims) {
|
|
107
154
|
const newSub = tokenEndpointResponse.claims.sub;
|
|
@@ -168,6 +215,18 @@ function updateStateDataForConnectionTokenSet(options, stateData, tokenEndpointR
|
|
|
168
215
|
var import_auth0_auth_js = require("@auth0/auth0-auth-js");
|
|
169
216
|
|
|
170
217
|
// src/utils.ts
|
|
218
|
+
var DEFAULT_SCOPES = "openid profile email offline_access";
|
|
219
|
+
var ensureOpenIdScope = (scope) => {
|
|
220
|
+
const normalizedScope = scope?.trim();
|
|
221
|
+
if (!normalizedScope) {
|
|
222
|
+
return DEFAULT_SCOPES;
|
|
223
|
+
}
|
|
224
|
+
const scopes = normalizedScope.split(/\s+/);
|
|
225
|
+
if (!scopes.includes("openid")) {
|
|
226
|
+
scopes.unshift("openid");
|
|
227
|
+
}
|
|
228
|
+
return scopes.join(" ");
|
|
229
|
+
};
|
|
171
230
|
var compareScopes = (scopes, requiredScopes) => {
|
|
172
231
|
if (scopes === requiredScopes) {
|
|
173
232
|
return true;
|
|
@@ -191,7 +250,7 @@ function getTelemetryConfig(config) {
|
|
|
191
250
|
return {
|
|
192
251
|
enabled: true,
|
|
193
252
|
name: config?.name ?? "@auth0/auth0-server-js",
|
|
194
|
-
version: config?.version ?? "1.
|
|
253
|
+
version: config?.version ?? "1.8.0"
|
|
195
254
|
};
|
|
196
255
|
}
|
|
197
256
|
|
|
@@ -253,9 +312,12 @@ var ServerMfaClient = class {
|
|
|
253
312
|
this.#options.stateStoreIdentifier,
|
|
254
313
|
storeOptions
|
|
255
314
|
);
|
|
256
|
-
const updatedStateData =
|
|
257
|
-
|
|
258
|
-
|
|
315
|
+
const updatedStateData = applySessionExpiryAtLogin(
|
|
316
|
+
updateStateData(audience, existingStateData, tokenResponse, {
|
|
317
|
+
domain: this.#options.domain
|
|
318
|
+
}),
|
|
319
|
+
tokenResponse.claims
|
|
320
|
+
);
|
|
259
321
|
await this.#options.stateStore.set(
|
|
260
322
|
this.#options.stateStoreIdentifier,
|
|
261
323
|
updatedStateData,
|
|
@@ -275,8 +337,99 @@ var ServerMfaClient = class {
|
|
|
275
337
|
}
|
|
276
338
|
};
|
|
277
339
|
|
|
340
|
+
// src/passkey/server-passkey-client.ts
|
|
341
|
+
var ServerPasskeyClient = class {
|
|
342
|
+
#options;
|
|
343
|
+
/**
|
|
344
|
+
* @internal
|
|
345
|
+
*/
|
|
346
|
+
constructor(options) {
|
|
347
|
+
this.#options = options;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Requests a passkey signup challenge for a new user.
|
|
351
|
+
*
|
|
352
|
+
* Returns the `authSession` and the WebAuthn credential creation options
|
|
353
|
+
* (`authnParamsPublicKey`). The application must return these to the browser,
|
|
354
|
+
* pass `authnParamsPublicKey` to `navigator.credentials.create()`, and then
|
|
355
|
+
* call `getToken()` with the resulting credential to complete signup.
|
|
356
|
+
*
|
|
357
|
+
* This method does not create a session; no state is persisted.
|
|
358
|
+
*
|
|
359
|
+
* @param options User profile data and optional realm/organization.
|
|
360
|
+
* @param storeOptions Optional options used to resolve the domain (resolver mode).
|
|
361
|
+
*
|
|
362
|
+
* @throws {PasskeyRegisterError} If there was an issue requesting the signup challenge.
|
|
363
|
+
*
|
|
364
|
+
* @returns A promise resolving to the signup challenge.
|
|
365
|
+
*/
|
|
366
|
+
async register(options, storeOptions) {
|
|
367
|
+
const domain = await this.#options.resolveDomain(storeOptions);
|
|
368
|
+
const authClient = this.#options.getAuthClient(domain);
|
|
369
|
+
return authClient.passkey.register(options);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Requests a passkey login challenge for an existing user.
|
|
373
|
+
*
|
|
374
|
+
* Returns the `authSession` and the WebAuthn credential request options
|
|
375
|
+
* (`authnParamsPublicKey`). The application must return these to the browser,
|
|
376
|
+
* pass `authnParamsPublicKey` to `navigator.credentials.get()`, and then
|
|
377
|
+
* call `getToken()` with the resulting credential to complete login.
|
|
378
|
+
*
|
|
379
|
+
* This method does not create a session; no state is persisted.
|
|
380
|
+
*
|
|
381
|
+
* @param options Optional realm/organization configuration.
|
|
382
|
+
* @param storeOptions Optional options used to resolve the domain (resolver mode).
|
|
383
|
+
*
|
|
384
|
+
* @throws {PasskeyChallengeError} If there was an issue requesting the login challenge.
|
|
385
|
+
*
|
|
386
|
+
* @returns A promise resolving to the login challenge.
|
|
387
|
+
*/
|
|
388
|
+
async challenge(options, storeOptions) {
|
|
389
|
+
const domain = await this.#options.resolveDomain(storeOptions);
|
|
390
|
+
const authClient = this.#options.getAuthClient(domain);
|
|
391
|
+
return authClient.passkey.challenge(options);
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Completes a passkey authentication flow (signup or login) by exchanging the
|
|
395
|
+
* WebAuthn credential for tokens, and persists the resulting session.
|
|
396
|
+
*
|
|
397
|
+
* Call this after obtaining a credential from `navigator.credentials.create()`
|
|
398
|
+
* (signup) or `navigator.credentials.get()` (login), passing the `authSession`
|
|
399
|
+
* returned by `register()` / `challenge()` together with the serialized credential.
|
|
400
|
+
*
|
|
401
|
+
* In resolver (multi-tenant) mode, pass the same `storeOptions` you passed to
|
|
402
|
+
* `register()` / `challenge()` so the token exchange resolves the same tenant
|
|
403
|
+
* that issued the `authSession`; otherwise the exchange will fail.
|
|
404
|
+
*
|
|
405
|
+
* @param options The auth session, serialized credential, and optional realm/scope/audience/organization.
|
|
406
|
+
* @param storeOptions Optional options used to pass to the State Store (and to resolve the domain in resolver mode).
|
|
407
|
+
*
|
|
408
|
+
* @throws {PasskeyGetTokenError} If there was an issue exchanging the credential for tokens. When the cause is `mfa_required`, use `isMfaRequiredError(error)` to narrow the error and read `cause.mfa_token`. No session is persisted in this case.
|
|
409
|
+
* @throws {OrganizationValidationError} When `organization` is passed and the returned ID token's organization claim is missing or does not match. The error is thrown before the session is written, so no session is persisted in this case.
|
|
410
|
+
*
|
|
411
|
+
* @returns A promise resolving to an object containing the authorizationDetails (when RAR was used).
|
|
412
|
+
*/
|
|
413
|
+
async getToken(options, storeOptions) {
|
|
414
|
+
const scope = ensureOpenIdScope(options.scope ?? this.#options.defaultScope);
|
|
415
|
+
const audience = options.audience ?? this.#options.defaultAudience;
|
|
416
|
+
const domain = await this.#options.resolveDomain(storeOptions);
|
|
417
|
+
const authClient = this.#options.getAuthClient(domain);
|
|
418
|
+
const tokenEndpointResponse = await authClient.passkey.getTokenByPasskey({
|
|
419
|
+
...options,
|
|
420
|
+
scope,
|
|
421
|
+
audience
|
|
422
|
+
});
|
|
423
|
+
const existingStateData = await this.#options.stateStore.get(this.#options.stateStoreIdentifier, storeOptions);
|
|
424
|
+
const stateData = updateStateData(audience ?? "default", existingStateData, tokenEndpointResponse, { domain });
|
|
425
|
+
await this.#options.stateStore.set(this.#options.stateStoreIdentifier, stateData, true, storeOptions);
|
|
426
|
+
return {
|
|
427
|
+
authorizationDetails: tokenEndpointResponse.authorizationDetails
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
278
432
|
// src/server-client.ts
|
|
279
|
-
var DEFAULT_SCOPES = "openid profile email offline_access";
|
|
280
433
|
var normalizeDomain = (value) => {
|
|
281
434
|
const trimmed = value.trim();
|
|
282
435
|
const parsed = trimmed.startsWith("http") ? new URL(trimmed) : new URL(`https://${trimmed}`);
|
|
@@ -290,16 +443,6 @@ var decodeIssuer = (token) => {
|
|
|
290
443
|
return void 0;
|
|
291
444
|
}
|
|
292
445
|
};
|
|
293
|
-
var ensureOpenIdScope = (scope) => {
|
|
294
|
-
if (!scope) {
|
|
295
|
-
return DEFAULT_SCOPES;
|
|
296
|
-
}
|
|
297
|
-
const scopes = scope.split(" ");
|
|
298
|
-
if (!scopes.includes("openid")) {
|
|
299
|
-
scopes.unshift("openid");
|
|
300
|
-
}
|
|
301
|
-
return scopes.join(" ");
|
|
302
|
-
};
|
|
303
446
|
var ServerClient = class {
|
|
304
447
|
#options;
|
|
305
448
|
#transactionStore;
|
|
@@ -310,6 +453,7 @@ var ServerClient = class {
|
|
|
310
453
|
#staticDomain;
|
|
311
454
|
#authClient;
|
|
312
455
|
#mfaClient;
|
|
456
|
+
#passkeyClient;
|
|
313
457
|
/**
|
|
314
458
|
* The underlying `authClient` instance that can be used to interact with the Auth0 Authentication API.
|
|
315
459
|
* Generally, you should prefer to use the higher-level methods exposed on the `ServerClient` instance.
|
|
@@ -344,6 +488,19 @@ var ServerClient = class {
|
|
|
344
488
|
}
|
|
345
489
|
return this.#mfaClient;
|
|
346
490
|
}
|
|
491
|
+
/**
|
|
492
|
+
* The passkey client for signing up and logging in users with WebAuthn credentials.
|
|
493
|
+
*
|
|
494
|
+
* Provides `register()` and `challenge()` to request signup/login challenges, and
|
|
495
|
+
* `getToken()` to exchange the resulting credential for tokens and persist the session.
|
|
496
|
+
*
|
|
497
|
+
* Unlike `mfa`, this property is available in both static and resolver (multi-tenant)
|
|
498
|
+
* domain modes. In resolver mode, pass the same `storeOptions` to `register()`/`challenge()`
|
|
499
|
+
* and `getToken()` so the credential is exchanged against the tenant that issued it.
|
|
500
|
+
*/
|
|
501
|
+
get passkey() {
|
|
502
|
+
return this.#passkeyClient;
|
|
503
|
+
}
|
|
347
504
|
constructor(options) {
|
|
348
505
|
this.#options = options;
|
|
349
506
|
this.#stateStoreIdentifier = this.#options.stateIdentifier || "__a0_session";
|
|
@@ -385,6 +542,14 @@ var ServerClient = class {
|
|
|
385
542
|
defaultAudience: this.#options.authorizationParams?.audience ?? "default"
|
|
386
543
|
});
|
|
387
544
|
}
|
|
545
|
+
this.#passkeyClient = new ServerPasskeyClient({
|
|
546
|
+
resolveDomain: (storeOptions) => this.#resolveDomain(storeOptions),
|
|
547
|
+
getAuthClient: (domain) => this.#getAuthClient(domain),
|
|
548
|
+
stateStore: this.#stateStore,
|
|
549
|
+
stateStoreIdentifier: this.#stateStoreIdentifier,
|
|
550
|
+
defaultScope: this.#options.authorizationParams?.scope,
|
|
551
|
+
defaultAudience: this.#options.authorizationParams?.audience
|
|
552
|
+
});
|
|
388
553
|
}
|
|
389
554
|
async #resolveDomain(storeOptions) {
|
|
390
555
|
if (typeof this.#options.domain === "function") {
|
|
@@ -478,6 +643,7 @@ var ServerClient = class {
|
|
|
478
643
|
*
|
|
479
644
|
* @throws {MissingTransactionError} When no transaction was found.
|
|
480
645
|
* @throws {TokenByCodeError} If there was an issue requesting the access token.
|
|
646
|
+
* @throws {SessionExpiredError} When the ID token's `session_expiry` is already in the past at login (the session is born expired); nothing is persisted.
|
|
481
647
|
*
|
|
482
648
|
* @returns A promise resolving to an object, containing the original appState (if present) and the authorizationDetails (when RAR was used).
|
|
483
649
|
*/
|
|
@@ -489,14 +655,18 @@ var ServerClient = class {
|
|
|
489
655
|
const domain = transactionData.domain ?? await this.#resolveDomain(storeOptions);
|
|
490
656
|
const authClient = this.#getAuthClient(domain);
|
|
491
657
|
const tokenEndpointResponse = await authClient.getTokenByCode(url, {
|
|
658
|
+
// TransactionData.codeVerifier is optional only to accommodate magic-link transactions.
|
|
492
659
|
codeVerifier: transactionData.codeVerifier
|
|
493
660
|
});
|
|
661
|
+
await this.#transactionStore.delete(this.#transactionStoreIdentifier, storeOptions);
|
|
494
662
|
const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
|
|
495
|
-
const stateData =
|
|
496
|
-
|
|
497
|
-
|
|
663
|
+
const stateData = applySessionExpiryAtLogin(
|
|
664
|
+
updateStateData(transactionData.audience ?? "default", existingStateData, tokenEndpointResponse, {
|
|
665
|
+
domain
|
|
666
|
+
}),
|
|
667
|
+
tokenEndpointResponse.claims
|
|
668
|
+
);
|
|
498
669
|
await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
|
|
499
|
-
await this.#transactionStore.delete(this.#transactionStoreIdentifier, storeOptions);
|
|
500
670
|
return { appState: transactionData.appState, authorizationDetails: tokenEndpointResponse.authorizationDetails };
|
|
501
671
|
}
|
|
502
672
|
/**
|
|
@@ -506,6 +676,7 @@ var ServerClient = class {
|
|
|
506
676
|
*
|
|
507
677
|
* @throws {MissingSessionError} If there is no active session.
|
|
508
678
|
* @throws {BuildLinkUserUrlError} If there was an issue when building the Authorization URL.
|
|
679
|
+
* @throws {SessionExpiredError} When the session's `session_expiry` ceiling has been reached; the session is cleared and re-authentication is required.
|
|
509
680
|
*
|
|
510
681
|
* @returns A promise resolving to a URL object, representing the URL to redirect the user-agent to to request authorization at Auth0.
|
|
511
682
|
*/
|
|
@@ -522,6 +693,10 @@ var ServerClient = class {
|
|
|
522
693
|
throw new MissingSessionError("Session domain does not match the current domain.");
|
|
523
694
|
}
|
|
524
695
|
}
|
|
696
|
+
if (isSessionExpiryReached(stateData.sessionExpiresAt)) {
|
|
697
|
+
await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
|
|
698
|
+
throw new SessionExpiredError();
|
|
699
|
+
}
|
|
525
700
|
const domain = this.#getSessionDomain(stateData);
|
|
526
701
|
const authClient = this.#getAuthClient(domain);
|
|
527
702
|
const { linkUserUrl, codeVerifier } = await authClient.buildLinkUserUrl({
|
|
@@ -565,6 +740,7 @@ var ServerClient = class {
|
|
|
565
740
|
*
|
|
566
741
|
* @throws {MissingSessionError} If there is no active session.
|
|
567
742
|
* @throws {BuildUnlinkUserUrlError} If there was an issue when building the User Unlinking URL.
|
|
743
|
+
* @throws {SessionExpiredError} When the session's `session_expiry` ceiling has been reached; the session is cleared and re-authentication is required.
|
|
568
744
|
*
|
|
569
745
|
* @returns A promise resolving to a URL object, representing the URL to redirect the user-agent to to request authorization at Auth0.
|
|
570
746
|
*/
|
|
@@ -581,6 +757,10 @@ var ServerClient = class {
|
|
|
581
757
|
throw new MissingSessionError("Session domain does not match the current domain.");
|
|
582
758
|
}
|
|
583
759
|
}
|
|
760
|
+
if (isSessionExpiryReached(stateData.sessionExpiresAt)) {
|
|
761
|
+
await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
|
|
762
|
+
throw new SessionExpiredError();
|
|
763
|
+
}
|
|
584
764
|
const domain = this.#getSessionDomain(stateData);
|
|
585
765
|
const authClient = this.#getAuthClient(domain);
|
|
586
766
|
const { unlinkUserUrl, codeVerifier } = await authClient.buildUnlinkUserUrl({
|
|
@@ -625,6 +805,7 @@ var ServerClient = class {
|
|
|
625
805
|
* @param storeOptions Optional options used to pass to the Transaction and State Store.
|
|
626
806
|
*
|
|
627
807
|
* @throws {BackchannelAuthenticationError} If there was an issue when doing backchannel authentication.
|
|
808
|
+
* @throws {SessionExpiredError} When the ID token's `session_expiry` is already in the past at login (the session is born expired); nothing is persisted.
|
|
628
809
|
*
|
|
629
810
|
* @returns A promise resolving to an object, containing the authorizationDetails (when RAR was used).
|
|
630
811
|
*/
|
|
@@ -641,6 +822,135 @@ var ServerClient = class {
|
|
|
641
822
|
}
|
|
642
823
|
});
|
|
643
824
|
const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
|
|
825
|
+
const stateData = applySessionExpiryAtLogin(
|
|
826
|
+
updateStateData(this.#options.authorizationParams?.audience ?? "default", existingStateData, tokenEndpointResponse, {
|
|
827
|
+
domain
|
|
828
|
+
}),
|
|
829
|
+
tokenEndpointResponse.claims
|
|
830
|
+
);
|
|
831
|
+
await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
|
|
832
|
+
return {
|
|
833
|
+
authorizationDetails: tokenEndpointResponse.authorizationDetails
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Starts a passwordless flow by sending a one-time code (OTP) or a magic link.
|
|
838
|
+
*
|
|
839
|
+
* Discriminated on `connection` (and, for email, `send`) to mirror the
|
|
840
|
+
* `@auth0/nextjs-auth0` `passwordless.start()` surface:
|
|
841
|
+
* - `{ connection: 'email' }` / `{ connection: 'email', send: 'code' }` — email OTP
|
|
842
|
+
* - `{ connection: 'email', send: 'link', redirectUri }` — email magic link
|
|
843
|
+
* - `{ connection: 'sms' }` — SMS OTP
|
|
844
|
+
*
|
|
845
|
+
* OTP modes are a stateless passthrough to the Authentication API (no session, no transaction);
|
|
846
|
+
* complete them with {@link ServerClient#completePasswordless}.
|
|
847
|
+
*
|
|
848
|
+
* Magic-link mode is stateful: the SDK generates an opaque anti-forgery `state`, sends the link
|
|
849
|
+
* with the OAuth parameters embedded (`redirect_uri`, `response_type=code`, `scope`, `state`),
|
|
850
|
+
* and persists a transaction carrying that `state`. NO PKCE challenge is registered, so the
|
|
851
|
+
* transaction holds no `codeVerifier`. Complete it with
|
|
852
|
+
* {@link ServerClient#completePasswordlessMagicLink}. Requires the tenant setting
|
|
853
|
+
* `allow_magiclink_verify_without_session: true` for server-side completion.
|
|
854
|
+
*
|
|
855
|
+
* @param options Discriminated start options.
|
|
856
|
+
* @param storeOptions Optional options passed to the resolver / stores.
|
|
857
|
+
*
|
|
858
|
+
* @throws {PasswordlessStartError} If the request fails, or if a magic link is requested without a `redirectUri`.
|
|
859
|
+
*
|
|
860
|
+
* @example
|
|
861
|
+
* // Email OTP
|
|
862
|
+
* await serverClient.startPasswordless({ connection: 'email', email: 'user@example.com' });
|
|
863
|
+
* // SMS OTP
|
|
864
|
+
* await serverClient.startPasswordless({ connection: 'sms', phoneNumber: '+14155550100' });
|
|
865
|
+
* // Email magic link
|
|
866
|
+
* await serverClient.startPasswordless({
|
|
867
|
+
* connection: 'email',
|
|
868
|
+
* email: 'user@example.com',
|
|
869
|
+
* send: 'link',
|
|
870
|
+
* redirectUri: 'https://app.example.com/auth/callback',
|
|
871
|
+
* });
|
|
872
|
+
*/
|
|
873
|
+
async startPasswordless(options, storeOptions) {
|
|
874
|
+
const domain = await this.#resolveDomain(storeOptions);
|
|
875
|
+
const authClient = this.#getAuthClient(domain);
|
|
876
|
+
if (options.connection === "sms") {
|
|
877
|
+
await authClient.passwordless.sendSms({
|
|
878
|
+
phoneNumber: options.phoneNumber,
|
|
879
|
+
language: options.language
|
|
880
|
+
});
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
if (options.send !== "link") {
|
|
884
|
+
await authClient.passwordless.sendEmail({
|
|
885
|
+
email: options.email,
|
|
886
|
+
send: "code",
|
|
887
|
+
language: options.language
|
|
888
|
+
});
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
if (!options.redirectUri || typeof options.redirectUri !== "string") {
|
|
892
|
+
throw new import_auth0_auth_js.PasswordlessStartError("redirectUri is required to start a passwordless magic-link login.");
|
|
893
|
+
}
|
|
894
|
+
const state = crypto.randomUUID();
|
|
895
|
+
const scope = ensureOpenIdScope(options.scope ?? this.#options.authorizationParams?.scope);
|
|
896
|
+
const audience = options.audience ?? this.#options.authorizationParams?.audience;
|
|
897
|
+
await authClient.passwordless.sendEmail({
|
|
898
|
+
email: options.email,
|
|
899
|
+
send: "link",
|
|
900
|
+
language: options.language,
|
|
901
|
+
authParams: {
|
|
902
|
+
...options.authParams,
|
|
903
|
+
redirect_uri: options.redirectUri,
|
|
904
|
+
response_type: "code",
|
|
905
|
+
scope,
|
|
906
|
+
...audience ? { audience } : {},
|
|
907
|
+
state
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
const transactionState = {
|
|
911
|
+
audience,
|
|
912
|
+
domain,
|
|
913
|
+
state
|
|
914
|
+
};
|
|
915
|
+
await this.#transactionStore.set(this.#transactionStoreIdentifier, transactionState, false, storeOptions);
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Completes a passwordless OTP login and persists the resulting session.
|
|
919
|
+
*
|
|
920
|
+
* Discriminated on `connection` to mirror the `@auth0/nextjs-auth0` `passwordless.verify()`
|
|
921
|
+
* surface. Non-redirect flow: no PKCE and no transaction store (mirrors
|
|
922
|
+
* {@link ServerClient#loginBackchannel}). The `openid` scope is always ensured by this layer.
|
|
923
|
+
*
|
|
924
|
+
* Note: the state store is read-then-written; if your deployment performs concurrent
|
|
925
|
+
* logins for the same session identifier, use a state store with atomic/serializable
|
|
926
|
+
* writes to avoid last-write-wins races.
|
|
927
|
+
*
|
|
928
|
+
* @param options Discriminated completion options (`connection`, identifier, `verificationCode`).
|
|
929
|
+
* @param storeOptions Optional options passed to the resolver / stores.
|
|
930
|
+
*
|
|
931
|
+
* @throws {PasswordlessVerifyError} If the code is invalid, expired, or rate-limited. When the
|
|
932
|
+
* connection requires MFA, the server responds with `mfa_required`; narrow the thrown error
|
|
933
|
+
* with `isMfaRequiredError(error)` to read `cause.mfa_token`.
|
|
934
|
+
*
|
|
935
|
+
* @returns A promise resolving to the authorizationDetails (when RAR was used).
|
|
936
|
+
*/
|
|
937
|
+
async completePasswordless(options, storeOptions) {
|
|
938
|
+
const scope = ensureOpenIdScope(options.authorizationParams?.scope ?? this.#options.authorizationParams?.scope);
|
|
939
|
+
const audience = options.authorizationParams?.audience ?? this.#options.authorizationParams?.audience;
|
|
940
|
+
const domain = await this.#resolveDomain(storeOptions);
|
|
941
|
+
const authClient = this.#getAuthClient(domain);
|
|
942
|
+
const tokenEndpointResponse = options.connection === "sms" ? await authClient.getTokenByPasswordlessSms({
|
|
943
|
+
phoneNumber: options.phoneNumber,
|
|
944
|
+
code: options.verificationCode,
|
|
945
|
+
audience,
|
|
946
|
+
scope
|
|
947
|
+
}) : await authClient.getTokenByPasswordlessEmail({
|
|
948
|
+
email: options.email,
|
|
949
|
+
code: options.verificationCode,
|
|
950
|
+
audience,
|
|
951
|
+
scope
|
|
952
|
+
});
|
|
953
|
+
const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
|
|
644
954
|
const stateData = updateStateData(
|
|
645
955
|
this.#options.authorizationParams?.audience ?? "default",
|
|
646
956
|
existingStateData,
|
|
@@ -652,6 +962,49 @@ var ServerClient = class {
|
|
|
652
962
|
authorizationDetails: tokenEndpointResponse.authorizationDetails
|
|
653
963
|
};
|
|
654
964
|
}
|
|
965
|
+
/**
|
|
966
|
+
* Completes a passwordless magic-link login and persists the resulting session.
|
|
967
|
+
*
|
|
968
|
+
* Loads the transaction persisted by {@link ServerClient#startPasswordless} (magic-link mode), validates the
|
|
969
|
+
* `state` returned on the callback URL against the stored `state` (anti-forgery binding), exchanges
|
|
970
|
+
* the authorization code WITHOUT PKCE, writes the session, and deletes the transaction. The existing
|
|
971
|
+
* interactive login path ({@link ServerClient#completeInteractiveLogin}) is not used.
|
|
972
|
+
*
|
|
973
|
+
* @param url The callback URL containing the authorization `code` and `state`.
|
|
974
|
+
* @param storeOptions Optional options passed to the resolver / stores.
|
|
975
|
+
*
|
|
976
|
+
* @throws {MissingTransactionError} If no magic-link transaction was found.
|
|
977
|
+
* @throws {PasswordlessVerifyError} If the returned `state` is missing or does not match.
|
|
978
|
+
* @throws {TokenByCodeError} If the token exchange fails.
|
|
979
|
+
*
|
|
980
|
+
* @returns A promise resolving to the authorizationDetails (when RAR was used).
|
|
981
|
+
*
|
|
982
|
+
* @example
|
|
983
|
+
* const result = await serverClient.completePasswordlessMagicLink(callbackUrl, storeOptions);
|
|
984
|
+
*/
|
|
985
|
+
async completePasswordlessMagicLink(url, storeOptions) {
|
|
986
|
+
const transactionData = await this.#transactionStore.get(this.#transactionStoreIdentifier, storeOptions);
|
|
987
|
+
if (!transactionData) {
|
|
988
|
+
throw new MissingTransactionError();
|
|
989
|
+
}
|
|
990
|
+
const expectedState = typeof transactionData.state === "string" ? transactionData.state : void 0;
|
|
991
|
+
const returnedState = url.searchParams.get("state");
|
|
992
|
+
if (!returnedState || !expectedState || returnedState !== expectedState) {
|
|
993
|
+
throw new import_auth0_auth_js.PasswordlessVerifyError("State mismatch on magic-link callback");
|
|
994
|
+
}
|
|
995
|
+
const domain = transactionData.domain ?? await this.#resolveDomain(storeOptions);
|
|
996
|
+
const authClient = this.#getAuthClient(domain);
|
|
997
|
+
const tokenEndpointResponse = await authClient.getTokenByMagicLinkCode(url, { expectedState });
|
|
998
|
+
const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
|
|
999
|
+
const stateData = updateStateData(transactionData.audience ?? "default", existingStateData, tokenEndpointResponse, {
|
|
1000
|
+
domain
|
|
1001
|
+
});
|
|
1002
|
+
await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
|
|
1003
|
+
await this.#transactionStore.delete(this.#transactionStoreIdentifier, storeOptions);
|
|
1004
|
+
return {
|
|
1005
|
+
authorizationDetails: tokenEndpointResponse.authorizationDetails
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
655
1008
|
/**
|
|
656
1009
|
* Retrieves the user from the store, or undefined if no user found.
|
|
657
1010
|
* @param storeOptions Optional options used to pass to the Transaction and State Store.
|
|
@@ -668,6 +1021,10 @@ var ServerClient = class {
|
|
|
668
1021
|
return;
|
|
669
1022
|
}
|
|
670
1023
|
}
|
|
1024
|
+
if (isSessionExpiryReached(stateData.sessionExpiresAt)) {
|
|
1025
|
+
await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
671
1028
|
return stateData.user;
|
|
672
1029
|
}
|
|
673
1030
|
/**
|
|
@@ -684,6 +1041,10 @@ var ServerClient = class {
|
|
|
684
1041
|
return;
|
|
685
1042
|
}
|
|
686
1043
|
}
|
|
1044
|
+
if (isSessionExpiryReached(stateData.sessionExpiresAt)) {
|
|
1045
|
+
await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
687
1048
|
const { internal, ...sessionData } = stateData;
|
|
688
1049
|
return sessionData;
|
|
689
1050
|
}
|
|
@@ -691,16 +1052,30 @@ var ServerClient = class {
|
|
|
691
1052
|
/**
|
|
692
1053
|
* Retrieves the access token from the store, or calls Auth0 when the access token is expired and a refresh token is available in the store.
|
|
693
1054
|
* Also updates the store when a new token was retrieved from Auth0.
|
|
1055
|
+
*
|
|
1056
|
+
* When `options.audience` and/or `options.scope` are provided, the SDK uses the session's refresh token to
|
|
1057
|
+
* request an access token for that audience/scope (Multi-Resource Refresh Tokens). Tokens are cached per
|
|
1058
|
+
* audience and scope combination.
|
|
1059
|
+
*
|
|
1060
|
+
* @param options Optional options for requesting a specific audience/scope.
|
|
694
1061
|
* @param storeOptions Optional options used to pass to the Transaction and State Store.
|
|
695
1062
|
*
|
|
696
1063
|
* @throws {TokenByRefreshTokenError} If the refresh token was not found or there was an issue requesting the access token. When the cause is `mfa_required`, use `isMfaRequiredError(error)` to narrow the error and read `cause.mfa_token`.
|
|
1064
|
+
* @throws {SessionExpiredError} When the session's `session_expiry` ceiling has been reached; the session is cleared and no refresh is attempted — the user must re-authenticate.
|
|
697
1065
|
*
|
|
698
1066
|
* @returns The Token Set, containing the access token, as well as additional information.
|
|
699
1067
|
*/
|
|
700
|
-
async getAccessToken(storeOptions) {
|
|
701
|
-
const
|
|
702
|
-
|
|
703
|
-
|
|
1068
|
+
async getAccessToken(tokenOptionsOrStoreOptions, storeOptions) {
|
|
1069
|
+
const hasTokenOptions = (
|
|
1070
|
+
// If second arg exists, first arg must be GetAccessTokenOptions
|
|
1071
|
+
storeOptions !== void 0 || // OR if first arg has audience/scope properties
|
|
1072
|
+
!!tokenOptionsOrStoreOptions && typeof tokenOptionsOrStoreOptions === "object" && ("audience" in tokenOptionsOrStoreOptions || "scope" in tokenOptionsOrStoreOptions)
|
|
1073
|
+
);
|
|
1074
|
+
const [resolvedOptions, resolvedStoreOptions] = hasTokenOptions ? [tokenOptionsOrStoreOptions, storeOptions] : [void 0, tokenOptionsOrStoreOptions];
|
|
1075
|
+
const stateData = await this.#stateStore.get(this.#stateStoreIdentifier, resolvedStoreOptions);
|
|
1076
|
+
const requestedAudience = resolvedOptions?.audience ?? this.#options.authorizationParams?.audience;
|
|
1077
|
+
const audience = requestedAudience ?? "default";
|
|
1078
|
+
const scope = resolvedOptions?.scope ?? this.#options.authorizationParams?.scope;
|
|
704
1079
|
const sessionDomain = stateData ? this.#getSessionDomain(stateData) : this.#staticDomain;
|
|
705
1080
|
if (this.#isResolverMode()) {
|
|
706
1081
|
if (!stateData) {
|
|
@@ -709,11 +1084,15 @@ var ServerClient = class {
|
|
|
709
1084
|
if (!sessionDomain) {
|
|
710
1085
|
throw new MissingSessionError("Session domain does not match the current domain.");
|
|
711
1086
|
}
|
|
712
|
-
const resolvedDomain = await this.#resolveDomain(
|
|
1087
|
+
const resolvedDomain = await this.#resolveDomain(resolvedStoreOptions);
|
|
713
1088
|
if (sessionDomain !== resolvedDomain) {
|
|
714
1089
|
throw new MissingSessionError("Session domain does not match the current domain.");
|
|
715
1090
|
}
|
|
716
1091
|
}
|
|
1092
|
+
if (stateData && isSessionExpiryReached(stateData.sessionExpiresAt)) {
|
|
1093
|
+
await this.#stateStore.delete(this.#stateStoreIdentifier, resolvedStoreOptions);
|
|
1094
|
+
throw new SessionExpiredError();
|
|
1095
|
+
}
|
|
717
1096
|
const tokenSet = stateData?.tokenSets.find(
|
|
718
1097
|
(tokenSet2) => tokenSet2.audience === audience && (!scope || compareScopes(tokenSet2.scope, scope))
|
|
719
1098
|
);
|
|
@@ -726,14 +1105,21 @@ var ServerClient = class {
|
|
|
726
1105
|
);
|
|
727
1106
|
}
|
|
728
1107
|
const domainForSession = sessionDomain;
|
|
729
|
-
const
|
|
730
|
-
refreshToken: stateData.refreshToken
|
|
731
|
-
|
|
732
|
-
|
|
1108
|
+
const tokenByRefreshTokenOptions = {
|
|
1109
|
+
refreshToken: stateData.refreshToken,
|
|
1110
|
+
// Only forward audience/scope to Auth0 when token options were explicitly supplied, and
|
|
1111
|
+
// never send the synthetic 'default' cache-key audience as a real request parameter.
|
|
1112
|
+
...hasTokenOptions && {
|
|
1113
|
+
...requestedAudience && { audience: requestedAudience },
|
|
1114
|
+
...scope && { scope }
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
const tokenEndpointResponse = await this.#getAuthClient(domainForSession).getTokenByRefreshToken(tokenByRefreshTokenOptions);
|
|
1118
|
+
const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, resolvedStoreOptions);
|
|
733
1119
|
const updatedStateData = updateStateData(audience, existingStateData, tokenEndpointResponse, {
|
|
734
1120
|
domain: domainForSession
|
|
735
1121
|
});
|
|
736
|
-
await this.#stateStore.set(this.#stateStoreIdentifier, updatedStateData, false,
|
|
1122
|
+
await this.#stateStore.set(this.#stateStoreIdentifier, updatedStateData, false, resolvedStoreOptions);
|
|
737
1123
|
return {
|
|
738
1124
|
accessToken: tokenEndpointResponse.accessToken,
|
|
739
1125
|
scope: tokenEndpointResponse.scope,
|
|
@@ -1041,7 +1427,7 @@ var AbstractTransactionStore = class extends AbstractStore {
|
|
|
1041
1427
|
};
|
|
1042
1428
|
|
|
1043
1429
|
// src/index.ts
|
|
1044
|
-
var
|
|
1430
|
+
var import_auth0_auth_js4 = require("@auth0/auth0-auth-js");
|
|
1045
1431
|
|
|
1046
1432
|
// src/store/cookie-transaction-store.ts
|
|
1047
1433
|
var CookieTransactionStore = class extends AbstractTransactionStore {
|
|
@@ -1081,14 +1467,14 @@ var AbstractSessionStore = class extends AbstractStateStore {
|
|
|
1081
1467
|
}
|
|
1082
1468
|
/**
|
|
1083
1469
|
* calculateMaxAge calculates the max age of the session based on createdAt and the rolling and absolute durations.
|
|
1470
|
+
* When sessionExpiresAt is provided, caps the maxAge to not exceed the time until that ceiling.
|
|
1084
1471
|
*/
|
|
1085
|
-
calculateMaxAge(createdAt) {
|
|
1086
|
-
if (!this.#rolling) {
|
|
1087
|
-
return this.#absoluteDuration;
|
|
1088
|
-
}
|
|
1472
|
+
calculateMaxAge(createdAt, sessionExpiresAt) {
|
|
1089
1473
|
const now = Date.now() / 1e3 | 0;
|
|
1090
|
-
|
|
1091
|
-
|
|
1474
|
+
let maxAge = this.#rolling ? Math.min(now + this.#inactivityDuration, createdAt + this.#absoluteDuration) - now : this.#absoluteDuration;
|
|
1475
|
+
if (sessionExpiresAt !== void 0) {
|
|
1476
|
+
maxAge = Math.min(maxAge, sessionExpiresAt - now);
|
|
1477
|
+
}
|
|
1092
1478
|
return maxAge > 0 ? maxAge : 0;
|
|
1093
1479
|
}
|
|
1094
1480
|
};
|
|
@@ -1116,7 +1502,7 @@ var StatefulStateStore = class extends AbstractSessionStore {
|
|
|
1116
1502
|
sessionId = generateId();
|
|
1117
1503
|
}
|
|
1118
1504
|
sessionId ??= generateId();
|
|
1119
|
-
const maxAge = this.calculateMaxAge(stateData.internal.createdAt);
|
|
1505
|
+
const maxAge = this.calculateMaxAge(stateData.internal.createdAt, stateData.sessionExpiresAt);
|
|
1120
1506
|
const cookieOpts = this.#getCookieOptions({
|
|
1121
1507
|
maxAge
|
|
1122
1508
|
});
|
|
@@ -1181,7 +1567,7 @@ var StatelessStateStore = class extends AbstractSessionStore {
|
|
|
1181
1567
|
this.#cookieHandler = cookieHandler;
|
|
1182
1568
|
}
|
|
1183
1569
|
async set(identifier, stateData, removeIfExists, options) {
|
|
1184
|
-
const maxAge = this.calculateMaxAge(stateData.internal.createdAt);
|
|
1570
|
+
const maxAge = this.calculateMaxAge(stateData.internal.createdAt, stateData.sessionExpiresAt);
|
|
1185
1571
|
const cookieOpts = this.#getCookieOptions({
|
|
1186
1572
|
maxAge
|
|
1187
1573
|
});
|
|
@@ -1239,6 +1625,9 @@ var StatelessStateStore = class extends AbstractSessionStore {
|
|
|
1239
1625
|
|
|
1240
1626
|
// src/mfa/index.ts
|
|
1241
1627
|
var import_auth0_auth_js2 = require("@auth0/auth0-auth-js");
|
|
1628
|
+
|
|
1629
|
+
// src/passkey/index.ts
|
|
1630
|
+
var import_auth0_auth_js3 = require("@auth0/auth0-auth-js");
|
|
1242
1631
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1243
1632
|
0 && (module.exports = {
|
|
1244
1633
|
AbstractStateStore,
|
|
@@ -1255,8 +1644,14 @@ var import_auth0_auth_js2 = require("@auth0/auth0-auth-js");
|
|
|
1255
1644
|
MissingRequiredArgumentError,
|
|
1256
1645
|
MissingSessionError,
|
|
1257
1646
|
MissingTransactionError,
|
|
1647
|
+
OrganizationValidationError,
|
|
1648
|
+
PasskeyChallengeError,
|
|
1649
|
+
PasskeyGetTokenError,
|
|
1650
|
+
PasskeyRegisterError,
|
|
1258
1651
|
ServerClient,
|
|
1259
1652
|
ServerMfaClient,
|
|
1653
|
+
ServerPasskeyClient,
|
|
1654
|
+
SessionExpiredError,
|
|
1260
1655
|
StartLinkUserError,
|
|
1261
1656
|
StatefulStateStore,
|
|
1262
1657
|
StatelessStateStore,
|