@auth0/auth0-server-js 1.2.0 → 1.4.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 +6 -1
- package/dist/index.cjs +344 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +89 -8
- package/dist/index.d.ts +89 -8
- package/dist/index.js +340 -47
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -34,6 +34,20 @@ var MissingSessionError = class extends Error {
|
|
|
34
34
|
this.name = "MissingSessionError";
|
|
35
35
|
}
|
|
36
36
|
};
|
|
37
|
+
var InvalidConfigurationError = class extends Error {
|
|
38
|
+
code = "invalid_configuration_error";
|
|
39
|
+
constructor(message) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = "InvalidConfigurationError";
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var IssuerValidationError = class extends Error {
|
|
45
|
+
code = "issuer_validation_error";
|
|
46
|
+
constructor(message) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = "IssuerValidationError";
|
|
49
|
+
}
|
|
50
|
+
};
|
|
37
51
|
|
|
38
52
|
// src/state/utils.ts
|
|
39
53
|
var createUpdatedTokenSet = (audience, response) => ({
|
|
@@ -42,19 +56,20 @@ var createUpdatedTokenSet = (audience, response) => ({
|
|
|
42
56
|
scope: response.scope,
|
|
43
57
|
expiresAt: response.expiresAt
|
|
44
58
|
});
|
|
45
|
-
function updateStateData(audience,
|
|
46
|
-
if (
|
|
47
|
-
const isNewTokenSet = !
|
|
59
|
+
function updateStateData(audience, stateData, tokenEndpointResponse, context) {
|
|
60
|
+
if (stateData) {
|
|
61
|
+
const isNewTokenSet = !stateData.tokenSets.some(
|
|
48
62
|
(tokenSet) => tokenSet.audience === audience && tokenSet.scope === tokenEndpointResponse.scope
|
|
49
63
|
);
|
|
50
|
-
const tokenSets = isNewTokenSet ? [...
|
|
64
|
+
const tokenSets = isNewTokenSet ? [...stateData.tokenSets, createUpdatedTokenSet(audience, tokenEndpointResponse)] : stateData.tokenSets.map(
|
|
51
65
|
(tokenSet) => tokenSet.audience === audience && tokenSet.scope === tokenEndpointResponse.scope ? createUpdatedTokenSet(audience, tokenEndpointResponse) : tokenSet
|
|
52
66
|
);
|
|
53
67
|
return {
|
|
54
|
-
...
|
|
55
|
-
idToken: tokenEndpointResponse.idToken,
|
|
56
|
-
refreshToken: tokenEndpointResponse.refreshToken ??
|
|
57
|
-
tokenSets
|
|
68
|
+
...stateData,
|
|
69
|
+
idToken: tokenEndpointResponse.idToken ?? stateData.idToken,
|
|
70
|
+
refreshToken: tokenEndpointResponse.refreshToken ?? stateData.refreshToken,
|
|
71
|
+
tokenSets,
|
|
72
|
+
domain: context?.domain ?? stateData.domain
|
|
58
73
|
};
|
|
59
74
|
} else {
|
|
60
75
|
const user = tokenEndpointResponse.claims;
|
|
@@ -62,9 +77,8 @@ function updateStateData(audience, stateDate, tokenEndpointResponse) {
|
|
|
62
77
|
user,
|
|
63
78
|
idToken: tokenEndpointResponse.idToken,
|
|
64
79
|
refreshToken: tokenEndpointResponse.refreshToken,
|
|
65
|
-
tokenSets: [
|
|
66
|
-
|
|
67
|
-
],
|
|
80
|
+
tokenSets: [createUpdatedTokenSet(audience, tokenEndpointResponse)],
|
|
81
|
+
domain: context?.domain,
|
|
68
82
|
internal: {
|
|
69
83
|
sid: user?.sid,
|
|
70
84
|
createdAt: Math.floor(Date.now() / 1e3)
|
|
@@ -72,9 +86,9 @@ function updateStateData(audience, stateDate, tokenEndpointResponse) {
|
|
|
72
86
|
};
|
|
73
87
|
}
|
|
74
88
|
}
|
|
75
|
-
function updateStateDataForConnectionTokenSet(options,
|
|
76
|
-
|
|
77
|
-
const isNewTokenSet = !
|
|
89
|
+
function updateStateDataForConnectionTokenSet(options, stateData, tokenEndpointResponse) {
|
|
90
|
+
stateData.connectionTokenSets = stateData.connectionTokenSets || [];
|
|
91
|
+
const isNewTokenSet = !stateData.connectionTokenSets.some(
|
|
78
92
|
(tokenSet) => tokenSet.connection === options.connection && (!options.loginHint || tokenSet.loginHint === options.loginHint)
|
|
79
93
|
);
|
|
80
94
|
const connectionTokenSet = {
|
|
@@ -84,11 +98,11 @@ function updateStateDataForConnectionTokenSet(options, stateDate, tokenEndpointR
|
|
|
84
98
|
scope: tokenEndpointResponse.scope,
|
|
85
99
|
expiresAt: tokenEndpointResponse.expiresAt
|
|
86
100
|
};
|
|
87
|
-
const connectionTokenSets = isNewTokenSet ? [...
|
|
101
|
+
const connectionTokenSets = isNewTokenSet ? [...stateData.connectionTokenSets, connectionTokenSet] : stateData.connectionTokenSets.map(
|
|
88
102
|
(tokenSet) => tokenSet.connection === options.connection && (!options.loginHint || tokenSet.loginHint === options.loginHint) ? connectionTokenSet : tokenSet
|
|
89
103
|
);
|
|
90
104
|
return {
|
|
91
|
-
...
|
|
105
|
+
...stateData,
|
|
92
106
|
connectionTokenSets
|
|
93
107
|
};
|
|
94
108
|
}
|
|
@@ -114,19 +128,70 @@ var compareScopes = (scopes, requiredScopes) => {
|
|
|
114
128
|
};
|
|
115
129
|
|
|
116
130
|
// src/server-client.ts
|
|
131
|
+
import { decodeJwt } from "jose";
|
|
132
|
+
|
|
133
|
+
// src/telemetry.ts
|
|
134
|
+
function getTelemetryConfig(config) {
|
|
135
|
+
if (config?.enabled === false) {
|
|
136
|
+
return config;
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
enabled: true,
|
|
140
|
+
name: config?.name ?? "@auth0/auth0-server-js",
|
|
141
|
+
version: config?.version ?? "1.4.0"
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/server-client.ts
|
|
146
|
+
var DEFAULT_SCOPES = "openid profile email offline_access";
|
|
147
|
+
var normalizeDomain = (value) => {
|
|
148
|
+
const trimmed = value.trim();
|
|
149
|
+
const parsed = trimmed.startsWith("http") ? new URL(trimmed) : new URL(`https://${trimmed}`);
|
|
150
|
+
return parsed.host.toLowerCase();
|
|
151
|
+
};
|
|
152
|
+
var decodeIssuer = (token) => {
|
|
153
|
+
try {
|
|
154
|
+
const { iss } = decodeJwt(token);
|
|
155
|
+
return typeof iss === "string" ? iss : void 0;
|
|
156
|
+
} catch {
|
|
157
|
+
return void 0;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
var ensureOpenIdScope = (scope) => {
|
|
161
|
+
if (!scope) {
|
|
162
|
+
return DEFAULT_SCOPES;
|
|
163
|
+
}
|
|
164
|
+
const scopes = scope.split(" ");
|
|
165
|
+
if (!scopes.includes("openid")) {
|
|
166
|
+
scopes.unshift("openid");
|
|
167
|
+
}
|
|
168
|
+
return scopes.join(" ");
|
|
169
|
+
};
|
|
117
170
|
var ServerClient = class {
|
|
118
171
|
#options;
|
|
119
172
|
#transactionStore;
|
|
120
173
|
#transactionStoreIdentifier;
|
|
121
174
|
#stateStore;
|
|
122
175
|
#stateStoreIdentifier;
|
|
176
|
+
#authClientOptions;
|
|
177
|
+
#staticDomain;
|
|
178
|
+
#authClient;
|
|
123
179
|
/**
|
|
124
180
|
* The underlying `authClient` instance that can be used to interact with the Auth0 Authentication API.
|
|
125
181
|
* Generally, you should prefer to use the higher-level methods exposed on the `ServerClient` instance.
|
|
126
|
-
*
|
|
182
|
+
*
|
|
183
|
+
* This property can only be used when `domain` is configured as a static string.
|
|
184
|
+
* In resolver mode (`domain` as a function), the SDK resolves the domain per request,
|
|
185
|
+
* so use `ServerClient` methods instead.
|
|
186
|
+
*
|
|
127
187
|
* Important: the methods exposed on the `authClient` instance do not handle any session or state management.
|
|
128
188
|
*/
|
|
129
|
-
authClient
|
|
189
|
+
get authClient() {
|
|
190
|
+
if (!this.#authClient) {
|
|
191
|
+
throw new InvalidConfigurationError("authClient is only available when using a static domain configuration.");
|
|
192
|
+
}
|
|
193
|
+
return this.#authClient;
|
|
194
|
+
}
|
|
130
195
|
constructor(options) {
|
|
131
196
|
this.#options = options;
|
|
132
197
|
this.#stateStoreIdentifier = this.#options.stateIdentifier || "__a0_session";
|
|
@@ -139,17 +204,77 @@ var ServerClient = class {
|
|
|
139
204
|
if (!this.#options.transactionStore) {
|
|
140
205
|
throw new MissingRequiredArgumentError("transactionStore");
|
|
141
206
|
}
|
|
142
|
-
this.
|
|
143
|
-
domain
|
|
207
|
+
if (typeof this.#options.domain !== "string" && typeof this.#options.domain !== "function") {
|
|
208
|
+
throw new InvalidConfigurationError("domain must be a string or resolver function");
|
|
209
|
+
}
|
|
210
|
+
this.#authClientOptions = {
|
|
144
211
|
clientId: this.#options.clientId,
|
|
145
212
|
clientSecret: this.#options.clientSecret,
|
|
146
213
|
clientAssertionSigningKey: this.#options.clientAssertionSigningKey,
|
|
147
214
|
clientAssertionSigningAlg: this.#options.clientAssertionSigningAlg,
|
|
148
215
|
authorizationParams: this.#options.authorizationParams,
|
|
216
|
+
discoveryCache: this.#options.discoveryCache,
|
|
149
217
|
customFetch: this.#options.customFetch,
|
|
150
218
|
useMtls: this.#options.useMtls
|
|
219
|
+
};
|
|
220
|
+
if (typeof this.#options.domain === "string") {
|
|
221
|
+
const domain = normalizeDomain(this.#options.domain);
|
|
222
|
+
this.#staticDomain = domain;
|
|
223
|
+
this.#authClient = new AuthClient({
|
|
224
|
+
domain,
|
|
225
|
+
...this.#authClientOptions,
|
|
226
|
+
telemetry: getTelemetryConfig(this.#options.telemetry)
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async #resolveDomain(storeOptions) {
|
|
231
|
+
if (typeof this.#options.domain === "function") {
|
|
232
|
+
const resolved = await this.#options.domain(storeOptions);
|
|
233
|
+
if (typeof resolved !== "string" || resolved.trim().length === 0) {
|
|
234
|
+
throw new InvalidConfigurationError("domainResolver returned no domain");
|
|
235
|
+
}
|
|
236
|
+
return normalizeDomain(resolved);
|
|
237
|
+
}
|
|
238
|
+
return normalizeDomain(this.#options.domain);
|
|
239
|
+
}
|
|
240
|
+
#createAuthClient(domain) {
|
|
241
|
+
return new AuthClient({
|
|
242
|
+
domain,
|
|
243
|
+
...this.#authClientOptions,
|
|
244
|
+
telemetry: getTelemetryConfig(this.#options.telemetry)
|
|
151
245
|
});
|
|
152
246
|
}
|
|
247
|
+
#getAuthClient(domain) {
|
|
248
|
+
const normalizedDomain = normalizeDomain(domain);
|
|
249
|
+
if (this.#authClient && this.#staticDomain === normalizedDomain) {
|
|
250
|
+
return this.#authClient;
|
|
251
|
+
}
|
|
252
|
+
return this.#createAuthClient(normalizedDomain);
|
|
253
|
+
}
|
|
254
|
+
#getSessionDomain(stateData) {
|
|
255
|
+
if (stateData.domain) {
|
|
256
|
+
return normalizeDomain(stateData.domain);
|
|
257
|
+
}
|
|
258
|
+
if (this.#staticDomain) {
|
|
259
|
+
return this.#staticDomain;
|
|
260
|
+
}
|
|
261
|
+
const issuerFromClaims = stateData.user?.iss;
|
|
262
|
+
if (typeof issuerFromClaims === "string" && issuerFromClaims.trim().length > 0) {
|
|
263
|
+
return normalizeDomain(issuerFromClaims);
|
|
264
|
+
}
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
#isResolverMode() {
|
|
268
|
+
return typeof this.#options.domain === "function";
|
|
269
|
+
}
|
|
270
|
+
async #isSessionForCurrentDomain(stateData, storeOptions) {
|
|
271
|
+
const sessionDomain = this.#getSessionDomain(stateData);
|
|
272
|
+
if (!sessionDomain) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
const resolvedDomain = await this.#resolveDomain(storeOptions);
|
|
276
|
+
return sessionDomain === resolvedDomain;
|
|
277
|
+
}
|
|
153
278
|
/**
|
|
154
279
|
* Starts the interactive login process, and returns a URL to redirect the user-agent to to request authorization at Auth0.
|
|
155
280
|
* @param options Optional options used to configure the interactive login process.
|
|
@@ -164,16 +289,21 @@ var ServerClient = class {
|
|
|
164
289
|
if (!redirectUri) {
|
|
165
290
|
throw new MissingRequiredArgumentError("authorizationParams.redirect_uri");
|
|
166
291
|
}
|
|
167
|
-
const
|
|
292
|
+
const scope = ensureOpenIdScope(options?.authorizationParams?.scope ?? this.#options.authorizationParams?.scope);
|
|
293
|
+
const domain = await this.#resolveDomain(storeOptions);
|
|
294
|
+
const authClient = this.#getAuthClient(domain);
|
|
295
|
+
const { codeVerifier, authorizationUrl } = await authClient.buildAuthorizationUrl({
|
|
168
296
|
pushedAuthorizationRequests: options?.pushedAuthorizationRequests,
|
|
169
297
|
authorizationParams: {
|
|
170
298
|
...options?.authorizationParams,
|
|
171
|
-
redirect_uri: redirectUri
|
|
299
|
+
redirect_uri: redirectUri,
|
|
300
|
+
scope
|
|
172
301
|
}
|
|
173
302
|
});
|
|
174
303
|
const transactionState = {
|
|
175
304
|
audience: options?.authorizationParams?.audience ?? this.#options.authorizationParams?.audience,
|
|
176
|
-
codeVerifier
|
|
305
|
+
codeVerifier,
|
|
306
|
+
domain
|
|
177
307
|
};
|
|
178
308
|
if (options?.appState) {
|
|
179
309
|
transactionState.appState = options.appState;
|
|
@@ -197,11 +327,15 @@ var ServerClient = class {
|
|
|
197
327
|
if (!transactionData) {
|
|
198
328
|
throw new MissingTransactionError();
|
|
199
329
|
}
|
|
200
|
-
const
|
|
330
|
+
const domain = transactionData.domain ?? await this.#resolveDomain(storeOptions);
|
|
331
|
+
const authClient = this.#getAuthClient(domain);
|
|
332
|
+
const tokenEndpointResponse = await authClient.getTokenByCode(url, {
|
|
201
333
|
codeVerifier: transactionData.codeVerifier
|
|
202
334
|
});
|
|
203
335
|
const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
|
|
204
|
-
const stateData = updateStateData(transactionData.audience ?? "default", existingStateData, tokenEndpointResponse
|
|
336
|
+
const stateData = updateStateData(transactionData.audience ?? "default", existingStateData, tokenEndpointResponse, {
|
|
337
|
+
domain
|
|
338
|
+
});
|
|
205
339
|
await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
|
|
206
340
|
await this.#transactionStore.delete(this.#transactionStoreIdentifier, storeOptions);
|
|
207
341
|
return { appState: transactionData.appState, authorizationDetails: tokenEndpointResponse.authorizationDetails };
|
|
@@ -223,7 +357,15 @@ var ServerClient = class {
|
|
|
223
357
|
"Unable to start the user linking process without a logged in user. Ensure to login using the SDK before starting the user linking process."
|
|
224
358
|
);
|
|
225
359
|
}
|
|
226
|
-
|
|
360
|
+
if (this.#isResolverMode()) {
|
|
361
|
+
const isCurrentDomain = await this.#isSessionForCurrentDomain(stateData, storeOptions);
|
|
362
|
+
if (!isCurrentDomain) {
|
|
363
|
+
throw new MissingSessionError("Session domain does not match the current domain.");
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const domain = this.#getSessionDomain(stateData);
|
|
367
|
+
const authClient = this.#getAuthClient(domain);
|
|
368
|
+
const { linkUserUrl, codeVerifier } = await authClient.buildLinkUserUrl({
|
|
227
369
|
connection: options.connection,
|
|
228
370
|
connectionScope: options.connectionScope,
|
|
229
371
|
idToken: stateData.idToken,
|
|
@@ -231,7 +373,8 @@ var ServerClient = class {
|
|
|
231
373
|
});
|
|
232
374
|
const transactionState = {
|
|
233
375
|
audience: options?.authorizationParams?.audience ?? this.#options.authorizationParams?.audience,
|
|
234
|
-
codeVerifier
|
|
376
|
+
codeVerifier,
|
|
377
|
+
domain
|
|
235
378
|
};
|
|
236
379
|
if (options?.appState) {
|
|
237
380
|
transactionState.appState = options.appState;
|
|
@@ -273,14 +416,23 @@ var ServerClient = class {
|
|
|
273
416
|
"Unable to start the user unlinking process without a logged in user. Ensure to login using the SDK before starting the user unlinking process."
|
|
274
417
|
);
|
|
275
418
|
}
|
|
276
|
-
|
|
419
|
+
if (this.#isResolverMode()) {
|
|
420
|
+
const isCurrentDomain = await this.#isSessionForCurrentDomain(stateData, storeOptions);
|
|
421
|
+
if (!isCurrentDomain) {
|
|
422
|
+
throw new MissingSessionError("Session domain does not match the current domain.");
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
const domain = this.#getSessionDomain(stateData);
|
|
426
|
+
const authClient = this.#getAuthClient(domain);
|
|
427
|
+
const { unlinkUserUrl, codeVerifier } = await authClient.buildUnlinkUserUrl({
|
|
277
428
|
connection: options.connection,
|
|
278
429
|
idToken: stateData.idToken,
|
|
279
430
|
authorizationParams: options.authorizationParams
|
|
280
431
|
});
|
|
281
432
|
const transactionState = {
|
|
282
433
|
audience: options?.authorizationParams?.audience ?? this.#options.authorizationParams?.audience,
|
|
283
|
-
codeVerifier
|
|
434
|
+
codeVerifier,
|
|
435
|
+
domain
|
|
284
436
|
};
|
|
285
437
|
if (options?.appState) {
|
|
286
438
|
transactionState.appState = options.appState;
|
|
@@ -318,16 +470,23 @@ var ServerClient = class {
|
|
|
318
470
|
* @returns A promise resolving to an object, containing the authorizationDetails (when RAR was used).
|
|
319
471
|
*/
|
|
320
472
|
async loginBackchannel(options, storeOptions) {
|
|
321
|
-
const
|
|
473
|
+
const scope = ensureOpenIdScope(options.authorizationParams?.scope ?? this.#options.authorizationParams?.scope);
|
|
474
|
+
const domain = await this.#resolveDomain(storeOptions);
|
|
475
|
+
const authClient = this.#getAuthClient(domain);
|
|
476
|
+
const tokenEndpointResponse = await authClient.backchannelAuthentication({
|
|
322
477
|
bindingMessage: options.bindingMessage,
|
|
323
478
|
loginHint: options.loginHint,
|
|
324
|
-
authorizationParams:
|
|
479
|
+
authorizationParams: {
|
|
480
|
+
...options.authorizationParams,
|
|
481
|
+
scope
|
|
482
|
+
}
|
|
325
483
|
});
|
|
326
484
|
const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
|
|
327
485
|
const stateData = updateStateData(
|
|
328
486
|
this.#options.authorizationParams?.audience ?? "default",
|
|
329
487
|
existingStateData,
|
|
330
|
-
tokenEndpointResponse
|
|
488
|
+
tokenEndpointResponse,
|
|
489
|
+
{ domain }
|
|
331
490
|
);
|
|
332
491
|
await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
|
|
333
492
|
return {
|
|
@@ -341,16 +500,31 @@ var ServerClient = class {
|
|
|
341
500
|
*/
|
|
342
501
|
async getUser(storeOptions) {
|
|
343
502
|
const stateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
|
|
344
|
-
|
|
503
|
+
if (!stateData) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (this.#isResolverMode()) {
|
|
507
|
+
const isCurrentDomain = await this.#isSessionForCurrentDomain(stateData, storeOptions);
|
|
508
|
+
if (!isCurrentDomain) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return stateData.user;
|
|
345
513
|
}
|
|
346
514
|
/**
|
|
347
515
|
* Retrieve the user session from the store, or undefined if no session found.
|
|
348
516
|
* @param storeOptions Optional options used to pass to the Transaction and State Store.
|
|
349
|
-
* @returns The
|
|
517
|
+
* @returns The session or undefined if no session found in the store.
|
|
350
518
|
*/
|
|
351
519
|
async getSession(storeOptions) {
|
|
352
520
|
const stateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
|
|
353
521
|
if (stateData) {
|
|
522
|
+
if (this.#isResolverMode()) {
|
|
523
|
+
const isCurrentDomain = await this.#isSessionForCurrentDomain(stateData, storeOptions);
|
|
524
|
+
if (!isCurrentDomain) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
354
528
|
const { internal, ...sessionData } = stateData;
|
|
355
529
|
return sessionData;
|
|
356
530
|
}
|
|
@@ -368,6 +542,19 @@ var ServerClient = class {
|
|
|
368
542
|
const stateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
|
|
369
543
|
const audience = this.#options.authorizationParams?.audience ?? "default";
|
|
370
544
|
const scope = this.#options.authorizationParams?.scope;
|
|
545
|
+
const sessionDomain = stateData ? this.#getSessionDomain(stateData) : this.#staticDomain;
|
|
546
|
+
if (this.#isResolverMode()) {
|
|
547
|
+
if (!stateData) {
|
|
548
|
+
throw new MissingSessionError("Unable to retrieve access token without a logged in user.");
|
|
549
|
+
}
|
|
550
|
+
if (!sessionDomain) {
|
|
551
|
+
throw new MissingSessionError("Session domain does not match the current domain.");
|
|
552
|
+
}
|
|
553
|
+
const resolvedDomain = await this.#resolveDomain(storeOptions);
|
|
554
|
+
if (sessionDomain !== resolvedDomain) {
|
|
555
|
+
throw new MissingSessionError("Session domain does not match the current domain.");
|
|
556
|
+
}
|
|
557
|
+
}
|
|
371
558
|
const tokenSet = stateData?.tokenSets.find(
|
|
372
559
|
(tokenSet2) => tokenSet2.audience === audience && (!scope || compareScopes(tokenSet2.scope, scope))
|
|
373
560
|
);
|
|
@@ -379,11 +566,14 @@ var ServerClient = class {
|
|
|
379
566
|
"The access token has expired and a refresh token was not provided. The user needs to re-authenticate."
|
|
380
567
|
);
|
|
381
568
|
}
|
|
382
|
-
const
|
|
569
|
+
const domainForSession = sessionDomain;
|
|
570
|
+
const tokenEndpointResponse = await this.#getAuthClient(domainForSession).getTokenByRefreshToken({
|
|
383
571
|
refreshToken: stateData.refreshToken
|
|
384
572
|
});
|
|
385
573
|
const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
|
|
386
|
-
const updatedStateData = updateStateData(audience, existingStateData, tokenEndpointResponse
|
|
574
|
+
const updatedStateData = updateStateData(audience, existingStateData, tokenEndpointResponse, {
|
|
575
|
+
domain: domainForSession
|
|
576
|
+
});
|
|
387
577
|
await this.#stateStore.set(this.#stateStoreIdentifier, updatedStateData, false, storeOptions);
|
|
388
578
|
return {
|
|
389
579
|
accessToken: tokenEndpointResponse.accessToken,
|
|
@@ -409,6 +599,19 @@ var ServerClient = class {
|
|
|
409
599
|
*/
|
|
410
600
|
async getAccessTokenForConnection(options, storeOptions) {
|
|
411
601
|
const stateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
|
|
602
|
+
const sessionDomain = stateData ? this.#getSessionDomain(stateData) : this.#staticDomain;
|
|
603
|
+
if (this.#isResolverMode()) {
|
|
604
|
+
if (!stateData) {
|
|
605
|
+
throw new MissingSessionError("Unable to retrieve an access token for a connection without a logged in user.");
|
|
606
|
+
}
|
|
607
|
+
if (!sessionDomain) {
|
|
608
|
+
throw new MissingSessionError("Session domain does not match the current domain.");
|
|
609
|
+
}
|
|
610
|
+
const resolvedDomain = await this.#resolveDomain(storeOptions);
|
|
611
|
+
if (sessionDomain !== resolvedDomain) {
|
|
612
|
+
throw new MissingSessionError("Session domain does not match the current domain.");
|
|
613
|
+
}
|
|
614
|
+
}
|
|
412
615
|
const connectionTokenSet = stateData?.connectionTokenSets?.find(
|
|
413
616
|
(tokenSet) => tokenSet.connection === options.connection
|
|
414
617
|
);
|
|
@@ -420,12 +623,20 @@ var ServerClient = class {
|
|
|
420
623
|
"A refresh token was not found but is required to be able to retrieve an access token for a connection."
|
|
421
624
|
);
|
|
422
625
|
}
|
|
423
|
-
const
|
|
626
|
+
const domainForSession = sessionDomain;
|
|
627
|
+
const tokenEndpointResponse = await this.#getAuthClient(domainForSession).getTokenForConnection({
|
|
424
628
|
connection: options.connection,
|
|
425
629
|
loginHint: options.loginHint,
|
|
426
630
|
refreshToken: stateData.refreshToken
|
|
427
631
|
});
|
|
428
|
-
const updatedStateData = updateStateDataForConnectionTokenSet(
|
|
632
|
+
const updatedStateData = updateStateDataForConnectionTokenSet(
|
|
633
|
+
options,
|
|
634
|
+
{
|
|
635
|
+
...stateData,
|
|
636
|
+
domain: stateData.domain ?? domainForSession
|
|
637
|
+
},
|
|
638
|
+
tokenEndpointResponse
|
|
639
|
+
);
|
|
429
640
|
await this.#stateStore.set(this.#stateStoreIdentifier, updatedStateData, false, storeOptions);
|
|
430
641
|
return {
|
|
431
642
|
accessToken: tokenEndpointResponse.accessToken,
|
|
@@ -442,8 +653,21 @@ var ServerClient = class {
|
|
|
442
653
|
* @returns {URL}
|
|
443
654
|
*/
|
|
444
655
|
async logout(options, storeOptions) {
|
|
445
|
-
|
|
446
|
-
|
|
656
|
+
if (!this.#isResolverMode()) {
|
|
657
|
+
await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
|
|
658
|
+
return this.authClient.buildLogoutUrl(options);
|
|
659
|
+
}
|
|
660
|
+
const resolvedDomain = await this.#resolveDomain(storeOptions);
|
|
661
|
+
const authClient = this.#getAuthClient(resolvedDomain);
|
|
662
|
+
const stateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
|
|
663
|
+
const sessionDomain = stateData ? this.#getSessionDomain(stateData) : void 0;
|
|
664
|
+
if (!stateData) {
|
|
665
|
+
return authClient.buildLogoutUrl(options);
|
|
666
|
+
}
|
|
667
|
+
if (sessionDomain && sessionDomain === resolvedDomain) {
|
|
668
|
+
await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
|
|
669
|
+
}
|
|
670
|
+
return authClient.buildLogoutUrl(options);
|
|
447
671
|
}
|
|
448
672
|
/**
|
|
449
673
|
* Handles the backchannel logout process by verifying the logout token and deleting the session from the store if the logout token was considered valid.
|
|
@@ -457,19 +681,37 @@ var ServerClient = class {
|
|
|
457
681
|
if (!logoutToken) {
|
|
458
682
|
throw new BackchannelLogoutError("Missing Logout Token");
|
|
459
683
|
}
|
|
460
|
-
|
|
461
|
-
|
|
684
|
+
if (!this.#isResolverMode()) {
|
|
685
|
+
const logoutTokenClaims2 = await this.authClient.verifyLogoutToken({ logoutToken });
|
|
686
|
+
await this.#stateStore.deleteByLogoutToken(logoutTokenClaims2, storeOptions);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const issuer = decodeIssuer(logoutToken);
|
|
690
|
+
if (!issuer) {
|
|
691
|
+
throw new BackchannelLogoutError("Logout token is missing an issuer");
|
|
692
|
+
}
|
|
693
|
+
const resolvedDomain = await this.#resolveDomain(storeOptions);
|
|
694
|
+
const domain = normalizeDomain(issuer);
|
|
695
|
+
if (domain !== resolvedDomain) {
|
|
696
|
+
throw new BackchannelLogoutError("Logout token issuer does not match the resolved domain");
|
|
697
|
+
}
|
|
698
|
+
const authClient = this.#getAuthClient(domain);
|
|
699
|
+
const logoutTokenClaims = await authClient.verifyLogoutToken({ logoutToken });
|
|
700
|
+
await this.#stateStore.deleteByLogoutToken({ ...logoutTokenClaims, iss: issuer }, storeOptions);
|
|
462
701
|
}
|
|
463
702
|
};
|
|
464
703
|
|
|
465
704
|
// src/encryption/index.ts
|
|
466
|
-
import { EncryptJWT, jwtDecrypt } from "jose";
|
|
705
|
+
import { EncryptJWT, jwtDecrypt, errors } from "jose";
|
|
467
706
|
var ENC = "A256CBC-HS512";
|
|
468
707
|
var ALG = "dir";
|
|
469
708
|
var DIGEST = "SHA-256";
|
|
470
709
|
var BIT_LENGTH = 512;
|
|
471
710
|
var HKDF_INFO = "derived cookie encryption secret";
|
|
472
711
|
var encoder;
|
|
712
|
+
function isDecryptionError(e) {
|
|
713
|
+
return e instanceof errors.JWEDecryptionFailed || e instanceof errors.JWEInvalid;
|
|
714
|
+
}
|
|
473
715
|
async function deriveEncryptionSecret(secret, salt, kid) {
|
|
474
716
|
encoder ||= new TextEncoder();
|
|
475
717
|
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), "HKDF", false, ["deriveBits"]);
|
|
@@ -486,12 +728,23 @@ async function deriveEncryptionSecret(secret, salt, kid) {
|
|
|
486
728
|
)
|
|
487
729
|
);
|
|
488
730
|
}
|
|
489
|
-
async function
|
|
731
|
+
async function _encrypt(payload, secret, salt, expiration) {
|
|
490
732
|
const kid = crypto.randomUUID();
|
|
491
733
|
const encryptionSecret = await deriveEncryptionSecret(secret, salt, kid);
|
|
492
734
|
return await new EncryptJWT(payload).setProtectedHeader({ enc: ENC, alg: ALG, kid }).setExpirationTime(expiration).encrypt(encryptionSecret);
|
|
493
735
|
}
|
|
494
|
-
async function
|
|
736
|
+
async function encrypt(payload, secret, salt, expiration) {
|
|
737
|
+
if (typeof secret === "string") {
|
|
738
|
+
return await _encrypt(payload, secret, salt, expiration);
|
|
739
|
+
} else {
|
|
740
|
+
const [newSecret] = secret;
|
|
741
|
+
if (!newSecret) {
|
|
742
|
+
throw new InvalidConfigurationError("At least one secret must be provided");
|
|
743
|
+
}
|
|
744
|
+
return await _encrypt(payload, newSecret, salt, expiration);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
async function _decrypt(value, secret, salt) {
|
|
495
748
|
const res = await jwtDecrypt(
|
|
496
749
|
value,
|
|
497
750
|
async (protectedHeader) => {
|
|
@@ -504,6 +757,35 @@ async function decrypt(value, secret, salt) {
|
|
|
504
757
|
);
|
|
505
758
|
return res.payload;
|
|
506
759
|
}
|
|
760
|
+
async function decrypt(value, secret, salt) {
|
|
761
|
+
if (typeof secret === "string") {
|
|
762
|
+
return await _decrypt(value, secret, salt);
|
|
763
|
+
} else {
|
|
764
|
+
const [newSecret, ...oldSecrets] = secret;
|
|
765
|
+
if (!newSecret) {
|
|
766
|
+
throw new InvalidConfigurationError("At least one secret must be provided");
|
|
767
|
+
}
|
|
768
|
+
let firstError;
|
|
769
|
+
try {
|
|
770
|
+
return await _decrypt(value, newSecret, salt);
|
|
771
|
+
} catch (err) {
|
|
772
|
+
firstError = err;
|
|
773
|
+
if (!isDecryptionError(err)) {
|
|
774
|
+
throw firstError;
|
|
775
|
+
}
|
|
776
|
+
for (const oldSecret of oldSecrets) {
|
|
777
|
+
try {
|
|
778
|
+
return await _decrypt(value, oldSecret, salt);
|
|
779
|
+
} catch (fallbackErr) {
|
|
780
|
+
if (!isDecryptionError(fallbackErr)) {
|
|
781
|
+
throw fallbackErr;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
throw firstError;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
507
789
|
|
|
508
790
|
// src/store/abstract-store.ts
|
|
509
791
|
var AbstractStore = class {
|
|
@@ -515,7 +797,14 @@ var AbstractStore = class {
|
|
|
515
797
|
return await encrypt(stateData, this.options.secret, identifier, expiration);
|
|
516
798
|
}
|
|
517
799
|
async decrypt(identifier, encryptedStateData) {
|
|
518
|
-
|
|
800
|
+
try {
|
|
801
|
+
return await decrypt(encryptedStateData, this.options.secret, identifier);
|
|
802
|
+
} catch (e) {
|
|
803
|
+
if (isDecryptionError(e)) {
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
throw e;
|
|
807
|
+
}
|
|
519
808
|
}
|
|
520
809
|
};
|
|
521
810
|
|
|
@@ -642,7 +931,9 @@ var StatefulStateStore = class extends AbstractSessionStore {
|
|
|
642
931
|
const cookieValue = this.#cookieHandler.getCookie(identifier, options);
|
|
643
932
|
if (cookieValue) {
|
|
644
933
|
const sessionCookie = await this.decrypt(identifier, cookieValue);
|
|
645
|
-
|
|
934
|
+
if (sessionCookie) {
|
|
935
|
+
return sessionCookie.id;
|
|
936
|
+
}
|
|
646
937
|
}
|
|
647
938
|
}
|
|
648
939
|
deleteByLogoutToken(claims, options) {
|
|
@@ -729,6 +1020,8 @@ export {
|
|
|
729
1020
|
AbstractTransactionStore,
|
|
730
1021
|
BackchannelLogoutError,
|
|
731
1022
|
CookieTransactionStore,
|
|
1023
|
+
InvalidConfigurationError,
|
|
1024
|
+
IssuerValidationError,
|
|
732
1025
|
MissingRequiredArgumentError,
|
|
733
1026
|
MissingSessionError,
|
|
734
1027
|
MissingTransactionError,
|