@better-auth/sso 1.4.7-beta.2 → 1.4.7-beta.4
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/.turbo/turbo-build.log +7 -7
- package/dist/client.d.mts +1 -1
- package/dist/{index-BWvN4yrs.d.mts → index-GoyGoP_a.d.mts} +390 -21
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +559 -63
- package/package.json +4 -4
- package/src/authn-request-store.ts +76 -0
- package/src/authn-request.test.ts +99 -0
- package/src/index.ts +46 -7
- package/src/oidc/discovery.test.ts +823 -0
- package/src/oidc/discovery.ts +355 -0
- package/src/oidc/errors.ts +86 -0
- package/src/oidc/index.ts +31 -0
- package/src/oidc/types.ts +210 -0
- package/src/oidc.test.ts +0 -164
- package/src/routes/sso.ts +415 -96
- package/src/saml.test.ts +781 -48
- package/src/types.ts +81 -0
package/dist/index.mjs
CHANGED
|
@@ -4,11 +4,51 @@ import { APIError, createAuthEndpoint, sessionMiddleware } from "better-auth/api
|
|
|
4
4
|
import { generateRandomString } from "better-auth/crypto";
|
|
5
5
|
import * as z from "zod/v4";
|
|
6
6
|
import { BetterFetchError, betterFetch } from "@better-fetch/fetch";
|
|
7
|
-
import { createAuthorizationURL, generateState, parseState, validateAuthorizationCode, validateToken } from "better-auth";
|
|
7
|
+
import { HIDE_METADATA, createAuthorizationURL, generateState, parseState, validateAuthorizationCode, validateToken } from "better-auth";
|
|
8
8
|
import { setSessionCookie } from "better-auth/cookies";
|
|
9
9
|
import { handleOAuthUserInfo } from "better-auth/oauth2";
|
|
10
10
|
import { decodeJwt } from "jose";
|
|
11
11
|
|
|
12
|
+
//#region src/authn-request-store.ts
|
|
13
|
+
/**
|
|
14
|
+
* Default TTL for AuthnRequest records (5 minutes).
|
|
15
|
+
* This should be sufficient for most IdPs while protecting against stale requests.
|
|
16
|
+
*/
|
|
17
|
+
const DEFAULT_AUTHN_REQUEST_TTL_MS = 300 * 1e3;
|
|
18
|
+
/**
|
|
19
|
+
* In-memory implementation of AuthnRequestStore.
|
|
20
|
+
* ⚠️ Only suitable for testing or single-instance non-serverless deployments.
|
|
21
|
+
* For production, rely on the default behavior (uses verification table)
|
|
22
|
+
* or provide a custom Redis-backed store.
|
|
23
|
+
*/
|
|
24
|
+
function createInMemoryAuthnRequestStore() {
|
|
25
|
+
const store = /* @__PURE__ */ new Map();
|
|
26
|
+
const cleanup = () => {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
for (const [id, record] of store.entries()) if (record.expiresAt < now) store.delete(id);
|
|
29
|
+
};
|
|
30
|
+
const cleanupInterval = setInterval(cleanup, 60 * 1e3);
|
|
31
|
+
if (typeof cleanupInterval.unref === "function") cleanupInterval.unref();
|
|
32
|
+
return {
|
|
33
|
+
async save(record) {
|
|
34
|
+
store.set(record.id, record);
|
|
35
|
+
},
|
|
36
|
+
async get(id) {
|
|
37
|
+
const record = store.get(id);
|
|
38
|
+
if (!record) return null;
|
|
39
|
+
if (record.expiresAt < Date.now()) {
|
|
40
|
+
store.delete(id);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return record;
|
|
44
|
+
},
|
|
45
|
+
async delete(id) {
|
|
46
|
+
store.delete(id);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
12
52
|
//#region src/routes/domain-verification.ts
|
|
13
53
|
const domainVerificationBodySchema = z.object({ providerId: z.string() });
|
|
14
54
|
const requestDomainVerification = (options) => {
|
|
@@ -183,6 +223,300 @@ const verifyDomain = (options) => {
|
|
|
183
223
|
});
|
|
184
224
|
};
|
|
185
225
|
|
|
226
|
+
//#endregion
|
|
227
|
+
//#region src/oidc/types.ts
|
|
228
|
+
/**
|
|
229
|
+
* Custom error class for OIDC discovery failures.
|
|
230
|
+
* Can be caught and mapped to APIError at the edge.
|
|
231
|
+
*/
|
|
232
|
+
var DiscoveryError = class DiscoveryError extends Error {
|
|
233
|
+
code;
|
|
234
|
+
details;
|
|
235
|
+
constructor(code, message, details, options) {
|
|
236
|
+
super(message, options);
|
|
237
|
+
this.name = "DiscoveryError";
|
|
238
|
+
this.code = code;
|
|
239
|
+
this.details = details;
|
|
240
|
+
if (Error.captureStackTrace) Error.captureStackTrace(this, DiscoveryError);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
/**
|
|
244
|
+
* Required fields that must be present in a valid discovery document.
|
|
245
|
+
*/
|
|
246
|
+
const REQUIRED_DISCOVERY_FIELDS = [
|
|
247
|
+
"issuer",
|
|
248
|
+
"authorization_endpoint",
|
|
249
|
+
"token_endpoint",
|
|
250
|
+
"jwks_uri"
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region src/oidc/discovery.ts
|
|
255
|
+
/**
|
|
256
|
+
* OIDC Discovery Pipeline
|
|
257
|
+
*
|
|
258
|
+
* Implements OIDC discovery document fetching, validation, and hydration.
|
|
259
|
+
* This module is used both at provider registration time (to persist validated config)
|
|
260
|
+
* and at runtime (to hydrate legacy providers that are missing metadata).
|
|
261
|
+
*
|
|
262
|
+
* @see https://openid.net/specs/openid-connect-discovery-1_0.html
|
|
263
|
+
*/
|
|
264
|
+
/** Default timeout for discovery requests (10 seconds) */
|
|
265
|
+
const DEFAULT_DISCOVERY_TIMEOUT = 1e4;
|
|
266
|
+
/**
|
|
267
|
+
* Main entry point: Discover and hydrate OIDC configuration from an issuer.
|
|
268
|
+
*
|
|
269
|
+
* This function:
|
|
270
|
+
* 1. Computes the discovery URL from the issuer
|
|
271
|
+
* 2. Validates the discovery URL (stub for now)
|
|
272
|
+
* 3. Fetches the discovery document
|
|
273
|
+
* 4. Validates the discovery document (issuer match + required fields)
|
|
274
|
+
* 5. Normalizes URLs (stub for now)
|
|
275
|
+
* 6. Selects token endpoint auth method
|
|
276
|
+
* 7. Merges with existing config (existing values take precedence)
|
|
277
|
+
*
|
|
278
|
+
* @param params - Discovery parameters
|
|
279
|
+
* @returns Hydrated OIDC configuration ready for persistence
|
|
280
|
+
* @throws DiscoveryError on any failure
|
|
281
|
+
*/
|
|
282
|
+
async function discoverOIDCConfig(params) {
|
|
283
|
+
const { issuer, existingConfig, timeout = DEFAULT_DISCOVERY_TIMEOUT } = params;
|
|
284
|
+
const discoveryUrl = params.discoveryEndpoint || existingConfig?.discoveryEndpoint || computeDiscoveryUrl(issuer);
|
|
285
|
+
validateDiscoveryUrl(discoveryUrl);
|
|
286
|
+
const discoveryDoc = await fetchDiscoveryDocument(discoveryUrl, timeout);
|
|
287
|
+
validateDiscoveryDocument(discoveryDoc, issuer);
|
|
288
|
+
const normalizedDoc = normalizeDiscoveryUrls(discoveryDoc, issuer);
|
|
289
|
+
const tokenEndpointAuth = selectTokenEndpointAuthMethod(normalizedDoc, existingConfig?.tokenEndpointAuthentication);
|
|
290
|
+
return {
|
|
291
|
+
issuer: existingConfig?.issuer ?? normalizedDoc.issuer,
|
|
292
|
+
discoveryEndpoint: existingConfig?.discoveryEndpoint ?? discoveryUrl,
|
|
293
|
+
authorizationEndpoint: existingConfig?.authorizationEndpoint ?? normalizedDoc.authorization_endpoint,
|
|
294
|
+
tokenEndpoint: existingConfig?.tokenEndpoint ?? normalizedDoc.token_endpoint,
|
|
295
|
+
jwksEndpoint: existingConfig?.jwksEndpoint ?? normalizedDoc.jwks_uri,
|
|
296
|
+
userInfoEndpoint: existingConfig?.userInfoEndpoint ?? normalizedDoc.userinfo_endpoint,
|
|
297
|
+
tokenEndpointAuthentication: existingConfig?.tokenEndpointAuthentication ?? tokenEndpointAuth,
|
|
298
|
+
scopesSupported: existingConfig?.scopesSupported ?? normalizedDoc.scopes_supported
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Compute the discovery URL from an issuer URL.
|
|
303
|
+
*
|
|
304
|
+
* Per OIDC Discovery spec, the discovery document is located at:
|
|
305
|
+
* <issuer>/.well-known/openid-configuration
|
|
306
|
+
*
|
|
307
|
+
* Handles trailing slashes correctly.
|
|
308
|
+
*/
|
|
309
|
+
function computeDiscoveryUrl(issuer) {
|
|
310
|
+
return `${issuer.endsWith("/") ? issuer.slice(0, -1) : issuer}/.well-known/openid-configuration`;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Validate a discovery URL before fetching.
|
|
314
|
+
*
|
|
315
|
+
* @param url - The discovery URL to validate
|
|
316
|
+
* @throws DiscoveryError if URL is invalid
|
|
317
|
+
*/
|
|
318
|
+
function validateDiscoveryUrl(url) {
|
|
319
|
+
try {
|
|
320
|
+
const parsed = new URL(url);
|
|
321
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") throw new DiscoveryError("discovery_invalid_url", `Discovery URL must use HTTP or HTTPS protocol: ${url}`, {
|
|
322
|
+
url,
|
|
323
|
+
protocol: parsed.protocol
|
|
324
|
+
});
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (error instanceof DiscoveryError) throw error;
|
|
327
|
+
throw new DiscoveryError("discovery_invalid_url", `Invalid discovery URL: ${url}`, { url }, { cause: error });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Fetch the OIDC discovery document from the IdP.
|
|
332
|
+
*
|
|
333
|
+
* @param url - The discovery endpoint URL
|
|
334
|
+
* @param timeout - Request timeout in milliseconds
|
|
335
|
+
* @returns The parsed discovery document
|
|
336
|
+
* @throws DiscoveryError on network errors, timeouts, or invalid responses
|
|
337
|
+
*/
|
|
338
|
+
async function fetchDiscoveryDocument(url, timeout = DEFAULT_DISCOVERY_TIMEOUT) {
|
|
339
|
+
try {
|
|
340
|
+
const response = await betterFetch(url, {
|
|
341
|
+
method: "GET",
|
|
342
|
+
timeout
|
|
343
|
+
});
|
|
344
|
+
if (response.error) {
|
|
345
|
+
const { status } = response.error;
|
|
346
|
+
if (status === 404) throw new DiscoveryError("discovery_not_found", "Discovery endpoint not found", {
|
|
347
|
+
url,
|
|
348
|
+
status
|
|
349
|
+
});
|
|
350
|
+
if (status === 408) throw new DiscoveryError("discovery_timeout", "Discovery request timed out", {
|
|
351
|
+
url,
|
|
352
|
+
timeout
|
|
353
|
+
});
|
|
354
|
+
throw new DiscoveryError("discovery_unexpected_error", `Unexpected discovery error: ${response.error.statusText}`, {
|
|
355
|
+
url,
|
|
356
|
+
...response.error
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
if (!response.data) throw new DiscoveryError("discovery_invalid_json", "Discovery endpoint returned an empty response", { url });
|
|
360
|
+
const data = response.data;
|
|
361
|
+
if (typeof data === "string") throw new DiscoveryError("discovery_invalid_json", "Discovery endpoint returned invalid JSON", {
|
|
362
|
+
url,
|
|
363
|
+
bodyPreview: data.slice(0, 200)
|
|
364
|
+
});
|
|
365
|
+
return data;
|
|
366
|
+
} catch (error) {
|
|
367
|
+
if (error instanceof DiscoveryError) throw error;
|
|
368
|
+
if (error instanceof Error && error.name === "AbortError") throw new DiscoveryError("discovery_timeout", "Discovery request timed out", {
|
|
369
|
+
url,
|
|
370
|
+
timeout
|
|
371
|
+
});
|
|
372
|
+
throw new DiscoveryError("discovery_unexpected_error", `Unexpected error during discovery: ${error instanceof Error ? error.message : String(error)}`, { url }, { cause: error });
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Validate a discovery document.
|
|
377
|
+
*
|
|
378
|
+
* Checks:
|
|
379
|
+
* 1. All required fields are present
|
|
380
|
+
* 2. Issuer matches the configured issuer (case-sensitive, exact match)
|
|
381
|
+
*
|
|
382
|
+
* Invariant: If this function returns without throwing, the document is safe
|
|
383
|
+
* to use for hydrating OIDC config (required fields present, issuer matches
|
|
384
|
+
* configured value, basic structural sanity verified).
|
|
385
|
+
*
|
|
386
|
+
* @param doc - The discovery document to validate
|
|
387
|
+
* @param configuredIssuer - The expected issuer value
|
|
388
|
+
* @throws DiscoveryError if validation fails
|
|
389
|
+
*/
|
|
390
|
+
function validateDiscoveryDocument(doc, configuredIssuer) {
|
|
391
|
+
const missingFields = [];
|
|
392
|
+
for (const field of REQUIRED_DISCOVERY_FIELDS) if (!doc[field]) missingFields.push(field);
|
|
393
|
+
if (missingFields.length > 0) throw new DiscoveryError("discovery_incomplete", `Discovery document is missing required fields: ${missingFields.join(", ")}`, { missingFields });
|
|
394
|
+
if ((doc.issuer.endsWith("/") ? doc.issuer.slice(0, -1) : doc.issuer) !== (configuredIssuer.endsWith("/") ? configuredIssuer.slice(0, -1) : configuredIssuer)) throw new DiscoveryError("issuer_mismatch", `Discovered issuer "${doc.issuer}" does not match configured issuer "${configuredIssuer}"`, {
|
|
395
|
+
discovered: doc.issuer,
|
|
396
|
+
configured: configuredIssuer
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Normalize URLs in the discovery document.
|
|
401
|
+
*
|
|
402
|
+
* @param doc - The discovery document
|
|
403
|
+
* @param _issuerBase - The base issuer URL
|
|
404
|
+
* @returns The normalized discovery document
|
|
405
|
+
*/
|
|
406
|
+
function normalizeDiscoveryUrls(doc, _issuerBase) {
|
|
407
|
+
return doc;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Normalize a single URL endpoint.
|
|
411
|
+
*
|
|
412
|
+
* @param endpoint - The endpoint URL to normalize
|
|
413
|
+
* @param _issuerBase - The base issuer URL
|
|
414
|
+
* @returns The normalized endpoint URL
|
|
415
|
+
*/
|
|
416
|
+
function normalizeUrl(endpoint, _issuerBase) {
|
|
417
|
+
return endpoint;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Select the token endpoint authentication method.
|
|
421
|
+
*
|
|
422
|
+
* @param doc - The discovery document
|
|
423
|
+
* @param existing - Existing authentication method from config
|
|
424
|
+
* @returns The selected authentication method
|
|
425
|
+
*/
|
|
426
|
+
function selectTokenEndpointAuthMethod(doc, existing) {
|
|
427
|
+
if (existing) return existing;
|
|
428
|
+
const supported = doc.token_endpoint_auth_methods_supported;
|
|
429
|
+
if (!supported || supported.length === 0) return "client_secret_basic";
|
|
430
|
+
if (supported.includes("client_secret_basic")) return "client_secret_basic";
|
|
431
|
+
if (supported.includes("client_secret_post")) return "client_secret_post";
|
|
432
|
+
return "client_secret_basic";
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Check if a provider configuration needs runtime discovery.
|
|
436
|
+
*
|
|
437
|
+
* Returns true if we need discovery at runtime to complete the token exchange
|
|
438
|
+
* and validation. Specifically checks for:
|
|
439
|
+
* - `tokenEndpoint` - required for exchanging authorization code for tokens
|
|
440
|
+
* - `jwksEndpoint` - required for validating ID token signatures
|
|
441
|
+
*
|
|
442
|
+
* Note: `authorizationEndpoint` is handled separately in the sign-in flow,
|
|
443
|
+
* so it's not checked here.
|
|
444
|
+
*
|
|
445
|
+
* @param config - Partial OIDC config from the provider
|
|
446
|
+
* @returns true if runtime discovery should be performed
|
|
447
|
+
*/
|
|
448
|
+
function needsRuntimeDiscovery(config) {
|
|
449
|
+
if (!config) return true;
|
|
450
|
+
return !config.tokenEndpoint || !config.jwksEndpoint;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
//#endregion
|
|
454
|
+
//#region src/oidc/errors.ts
|
|
455
|
+
/**
|
|
456
|
+
* OIDC Discovery Error Mapping
|
|
457
|
+
*
|
|
458
|
+
* Maps DiscoveryError codes to appropriate APIError responses.
|
|
459
|
+
* Used at the boundary between the discovery pipeline and HTTP handlers.
|
|
460
|
+
*/
|
|
461
|
+
/**
|
|
462
|
+
* Maps a DiscoveryError to an appropriate APIError for HTTP responses.
|
|
463
|
+
*
|
|
464
|
+
* Error code mapping:
|
|
465
|
+
* - discovery_invalid_url → 400 BAD_REQUEST
|
|
466
|
+
* - discovery_not_found → 400 BAD_REQUEST
|
|
467
|
+
* - discovery_invalid_json → 400 BAD_REQUEST
|
|
468
|
+
* - discovery_incomplete → 400 BAD_REQUEST
|
|
469
|
+
* - issuer_mismatch → 400 BAD_REQUEST
|
|
470
|
+
* - unsupported_token_auth_method → 400 BAD_REQUEST
|
|
471
|
+
* - discovery_timeout → 502 BAD_GATEWAY
|
|
472
|
+
* - discovery_unexpected_error → 502 BAD_GATEWAY
|
|
473
|
+
*
|
|
474
|
+
* @param error - The DiscoveryError to map
|
|
475
|
+
* @returns An APIError with appropriate status and message
|
|
476
|
+
*/
|
|
477
|
+
function mapDiscoveryErrorToAPIError(error) {
|
|
478
|
+
switch (error.code) {
|
|
479
|
+
case "discovery_timeout": return new APIError("BAD_GATEWAY", {
|
|
480
|
+
message: `OIDC discovery timed out: ${error.message}`,
|
|
481
|
+
code: error.code
|
|
482
|
+
});
|
|
483
|
+
case "discovery_unexpected_error": return new APIError("BAD_GATEWAY", {
|
|
484
|
+
message: `OIDC discovery failed: ${error.message}`,
|
|
485
|
+
code: error.code
|
|
486
|
+
});
|
|
487
|
+
case "discovery_not_found": return new APIError("BAD_REQUEST", {
|
|
488
|
+
message: `OIDC discovery endpoint not found. The issuer may not support OIDC discovery, or the URL is incorrect. ${error.message}`,
|
|
489
|
+
code: error.code
|
|
490
|
+
});
|
|
491
|
+
case "discovery_invalid_url": return new APIError("BAD_REQUEST", {
|
|
492
|
+
message: `Invalid OIDC discovery URL: ${error.message}`,
|
|
493
|
+
code: error.code
|
|
494
|
+
});
|
|
495
|
+
case "discovery_invalid_json": return new APIError("BAD_REQUEST", {
|
|
496
|
+
message: `OIDC discovery returned invalid data: ${error.message}`,
|
|
497
|
+
code: error.code
|
|
498
|
+
});
|
|
499
|
+
case "discovery_incomplete": return new APIError("BAD_REQUEST", {
|
|
500
|
+
message: `OIDC discovery document is missing required fields: ${error.message}`,
|
|
501
|
+
code: error.code
|
|
502
|
+
});
|
|
503
|
+
case "issuer_mismatch": return new APIError("BAD_REQUEST", {
|
|
504
|
+
message: `OIDC issuer mismatch: ${error.message}`,
|
|
505
|
+
code: error.code
|
|
506
|
+
});
|
|
507
|
+
case "unsupported_token_auth_method": return new APIError("BAD_REQUEST", {
|
|
508
|
+
message: `Incompatible OIDC provider: ${error.message}`,
|
|
509
|
+
code: error.code
|
|
510
|
+
});
|
|
511
|
+
default:
|
|
512
|
+
error.code;
|
|
513
|
+
return new APIError("INTERNAL_SERVER_ERROR", {
|
|
514
|
+
message: `Unexpected discovery error: ${error.message}`,
|
|
515
|
+
code: "discovery_unexpected_error"
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
186
520
|
//#endregion
|
|
187
521
|
//#region src/utils.ts
|
|
188
522
|
/**
|
|
@@ -213,6 +547,48 @@ const validateEmailDomain = (email, domain) => {
|
|
|
213
547
|
|
|
214
548
|
//#endregion
|
|
215
549
|
//#region src/routes/sso.ts
|
|
550
|
+
const AUTHN_REQUEST_KEY_PREFIX = "saml-authn-request:";
|
|
551
|
+
/** Default clock skew tolerance: 5 minutes */
|
|
552
|
+
const DEFAULT_CLOCK_SKEW_MS = 300 * 1e3;
|
|
553
|
+
/**
|
|
554
|
+
* Validates SAML assertion timestamp conditions (NotBefore/NotOnOrAfter).
|
|
555
|
+
* Prevents acceptance of expired or future-dated assertions.
|
|
556
|
+
* @throws {APIError} If timestamps are invalid, expired, or not yet valid
|
|
557
|
+
*/
|
|
558
|
+
function validateSAMLTimestamp(conditions, options = {}) {
|
|
559
|
+
const clockSkew = options.clockSkew ?? DEFAULT_CLOCK_SKEW_MS;
|
|
560
|
+
if (!(conditions?.notBefore || conditions?.notOnOrAfter)) {
|
|
561
|
+
if (options.requireTimestamps) throw new APIError("BAD_REQUEST", {
|
|
562
|
+
message: "SAML assertion missing required timestamp conditions",
|
|
563
|
+
details: "Assertions must include NotBefore and/or NotOnOrAfter conditions"
|
|
564
|
+
});
|
|
565
|
+
options.logger?.warn("SAML assertion accepted without timestamp conditions", { hasConditions: !!conditions });
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
const now = Date.now();
|
|
569
|
+
if (conditions?.notBefore) {
|
|
570
|
+
const notBeforeTime = new Date(conditions.notBefore).getTime();
|
|
571
|
+
if (Number.isNaN(notBeforeTime)) throw new APIError("BAD_REQUEST", {
|
|
572
|
+
message: "SAML assertion has invalid NotBefore timestamp",
|
|
573
|
+
details: `Unable to parse NotBefore value: ${conditions.notBefore}`
|
|
574
|
+
});
|
|
575
|
+
if (now < notBeforeTime - clockSkew) throw new APIError("BAD_REQUEST", {
|
|
576
|
+
message: "SAML assertion is not yet valid",
|
|
577
|
+
details: `Current time is before NotBefore (with ${clockSkew}ms clock skew tolerance)`
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
if (conditions?.notOnOrAfter) {
|
|
581
|
+
const notOnOrAfterTime = new Date(conditions.notOnOrAfter).getTime();
|
|
582
|
+
if (Number.isNaN(notOnOrAfterTime)) throw new APIError("BAD_REQUEST", {
|
|
583
|
+
message: "SAML assertion has invalid NotOnOrAfter timestamp",
|
|
584
|
+
details: `Unable to parse NotOnOrAfter value: ${conditions.notOnOrAfter}`
|
|
585
|
+
});
|
|
586
|
+
if (now > notOnOrAfterTime + clockSkew) throw new APIError("BAD_REQUEST", {
|
|
587
|
+
message: "SAML assertion has expired",
|
|
588
|
+
details: `Current time is after NotOnOrAfter (with ${clockSkew}ms clock skew tolerance)`
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
216
592
|
const spMetadataQuerySchema = z.object({
|
|
217
593
|
providerId: z.string(),
|
|
218
594
|
format: z.enum(["xml", "json"]).default("xml")
|
|
@@ -263,6 +639,7 @@ const ssoProviderBodySchema = z.object({
|
|
|
263
639
|
tokenEndpointAuthentication: z.enum(["client_secret_post", "client_secret_basic"]).optional(),
|
|
264
640
|
jwksEndpoint: z.string({}).meta({ description: "The JWKS endpoint" }).optional(),
|
|
265
641
|
discoveryEndpoint: z.string().optional(),
|
|
642
|
+
skipDiscovery: z.boolean().meta({ description: "Skip OIDC discovery during registration. When true, you must provide authorizationEndpoint, tokenEndpoint, and jwksEndpoint manually." }).optional(),
|
|
266
643
|
scopes: z.array(z.string(), {}).meta({ description: "The scopes to request. Defaults to ['openid', 'email', 'profile', 'offline_access']" }).optional(),
|
|
267
644
|
pkce: z.boolean({}).meta({ description: "Whether to use PKCE for the authorization flow" }).default(true).optional(),
|
|
268
645
|
mapping: z.object({
|
|
@@ -530,27 +907,64 @@ const registerSSOProvider = (options) => {
|
|
|
530
907
|
ctx.context.logger.info(`SSO provider creation attempt with existing providerId: ${body.providerId}`);
|
|
531
908
|
throw new APIError("UNPROCESSABLE_ENTITY", { message: "SSO provider with this providerId already exists" });
|
|
532
909
|
}
|
|
910
|
+
let hydratedOIDCConfig = null;
|
|
911
|
+
if (body.oidcConfig && !body.oidcConfig.skipDiscovery) try {
|
|
912
|
+
hydratedOIDCConfig = await discoverOIDCConfig({
|
|
913
|
+
issuer: body.issuer,
|
|
914
|
+
existingConfig: {
|
|
915
|
+
discoveryEndpoint: body.oidcConfig.discoveryEndpoint,
|
|
916
|
+
authorizationEndpoint: body.oidcConfig.authorizationEndpoint,
|
|
917
|
+
tokenEndpoint: body.oidcConfig.tokenEndpoint,
|
|
918
|
+
jwksEndpoint: body.oidcConfig.jwksEndpoint,
|
|
919
|
+
userInfoEndpoint: body.oidcConfig.userInfoEndpoint,
|
|
920
|
+
tokenEndpointAuthentication: body.oidcConfig.tokenEndpointAuthentication
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
} catch (error) {
|
|
924
|
+
if (error instanceof DiscoveryError) throw mapDiscoveryErrorToAPIError(error);
|
|
925
|
+
throw error;
|
|
926
|
+
}
|
|
927
|
+
const buildOIDCConfig = () => {
|
|
928
|
+
if (!body.oidcConfig) return null;
|
|
929
|
+
if (body.oidcConfig.skipDiscovery) return JSON.stringify({
|
|
930
|
+
issuer: body.issuer,
|
|
931
|
+
clientId: body.oidcConfig.clientId,
|
|
932
|
+
clientSecret: body.oidcConfig.clientSecret,
|
|
933
|
+
authorizationEndpoint: body.oidcConfig.authorizationEndpoint,
|
|
934
|
+
tokenEndpoint: body.oidcConfig.tokenEndpoint,
|
|
935
|
+
tokenEndpointAuthentication: body.oidcConfig.tokenEndpointAuthentication || "client_secret_basic",
|
|
936
|
+
jwksEndpoint: body.oidcConfig.jwksEndpoint,
|
|
937
|
+
pkce: body.oidcConfig.pkce,
|
|
938
|
+
discoveryEndpoint: body.oidcConfig.discoveryEndpoint || `${body.issuer}/.well-known/openid-configuration`,
|
|
939
|
+
mapping: body.oidcConfig.mapping,
|
|
940
|
+
scopes: body.oidcConfig.scopes,
|
|
941
|
+
userInfoEndpoint: body.oidcConfig.userInfoEndpoint,
|
|
942
|
+
overrideUserInfo: ctx.body.overrideUserInfo || options?.defaultOverrideUserInfo || false
|
|
943
|
+
});
|
|
944
|
+
if (!hydratedOIDCConfig) return null;
|
|
945
|
+
return JSON.stringify({
|
|
946
|
+
issuer: hydratedOIDCConfig.issuer,
|
|
947
|
+
clientId: body.oidcConfig.clientId,
|
|
948
|
+
clientSecret: body.oidcConfig.clientSecret,
|
|
949
|
+
authorizationEndpoint: hydratedOIDCConfig.authorizationEndpoint,
|
|
950
|
+
tokenEndpoint: hydratedOIDCConfig.tokenEndpoint,
|
|
951
|
+
tokenEndpointAuthentication: hydratedOIDCConfig.tokenEndpointAuthentication,
|
|
952
|
+
jwksEndpoint: hydratedOIDCConfig.jwksEndpoint,
|
|
953
|
+
pkce: body.oidcConfig.pkce,
|
|
954
|
+
discoveryEndpoint: hydratedOIDCConfig.discoveryEndpoint,
|
|
955
|
+
mapping: body.oidcConfig.mapping,
|
|
956
|
+
scopes: body.oidcConfig.scopes,
|
|
957
|
+
userInfoEndpoint: hydratedOIDCConfig.userInfoEndpoint,
|
|
958
|
+
overrideUserInfo: ctx.body.overrideUserInfo || options?.defaultOverrideUserInfo || false
|
|
959
|
+
});
|
|
960
|
+
};
|
|
533
961
|
const provider = await ctx.context.adapter.create({
|
|
534
962
|
model: "ssoProvider",
|
|
535
963
|
data: {
|
|
536
964
|
issuer: body.issuer,
|
|
537
965
|
domain: body.domain,
|
|
538
966
|
domainVerified: false,
|
|
539
|
-
oidcConfig:
|
|
540
|
-
issuer: body.issuer,
|
|
541
|
-
clientId: body.oidcConfig.clientId,
|
|
542
|
-
clientSecret: body.oidcConfig.clientSecret,
|
|
543
|
-
authorizationEndpoint: body.oidcConfig.authorizationEndpoint,
|
|
544
|
-
tokenEndpoint: body.oidcConfig.tokenEndpoint,
|
|
545
|
-
tokenEndpointAuthentication: body.oidcConfig.tokenEndpointAuthentication,
|
|
546
|
-
jwksEndpoint: body.oidcConfig.jwksEndpoint,
|
|
547
|
-
pkce: body.oidcConfig.pkce,
|
|
548
|
-
discoveryEndpoint: body.oidcConfig.discoveryEndpoint || `${body.issuer}/.well-known/openid-configuration`,
|
|
549
|
-
mapping: body.oidcConfig.mapping,
|
|
550
|
-
scopes: body.oidcConfig.scopes,
|
|
551
|
-
userInfoEndpoint: body.oidcConfig.userInfoEndpoint,
|
|
552
|
-
overrideUserInfo: ctx.body.overrideUserInfo || options?.defaultOverrideUserInfo || false
|
|
553
|
-
}) : null,
|
|
967
|
+
oidcConfig: buildOIDCConfig(),
|
|
554
968
|
samlConfig: body.samlConfig ? JSON.stringify({
|
|
555
969
|
issuer: body.issuer,
|
|
556
970
|
entryPoint: body.samlConfig.entryPoint,
|
|
@@ -781,6 +1195,21 @@ const signInSSO = (options) => {
|
|
|
781
1195
|
});
|
|
782
1196
|
const loginRequest = sp.createLoginRequest(idp, "redirect");
|
|
783
1197
|
if (!loginRequest) throw new APIError("BAD_REQUEST", { message: "Invalid SAML request" });
|
|
1198
|
+
if (loginRequest.id && (options?.saml?.authnRequestStore || options?.saml?.enableInResponseToValidation)) {
|
|
1199
|
+
const ttl = options?.saml?.requestTTL ?? DEFAULT_AUTHN_REQUEST_TTL_MS;
|
|
1200
|
+
const record = {
|
|
1201
|
+
id: loginRequest.id,
|
|
1202
|
+
providerId: provider.providerId,
|
|
1203
|
+
createdAt: Date.now(),
|
|
1204
|
+
expiresAt: Date.now() + ttl
|
|
1205
|
+
};
|
|
1206
|
+
if (options?.saml?.authnRequestStore) await options.saml.authnRequestStore.save(record);
|
|
1207
|
+
else await ctx.context.internalAdapter.createVerificationValue({
|
|
1208
|
+
identifier: `${AUTHN_REQUEST_KEY_PREFIX}${record.id}`,
|
|
1209
|
+
value: JSON.stringify(record),
|
|
1210
|
+
expiresAt: new Date(record.expiresAt)
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
784
1213
|
return ctx.json({
|
|
785
1214
|
url: `${loginRequest.context}&RelayState=${encodeURIComponent(body.callbackURL)}`,
|
|
786
1215
|
redirect: true
|
|
@@ -801,7 +1230,7 @@ const callbackSSO = (options) => {
|
|
|
801
1230
|
query: callbackSSOQuerySchema,
|
|
802
1231
|
allowedMediaTypes: ["application/x-www-form-urlencoded", "application/json"],
|
|
803
1232
|
metadata: {
|
|
804
|
-
|
|
1233
|
+
...HIDE_METADATA,
|
|
805
1234
|
openapi: {
|
|
806
1235
|
operationId: "handleSSOCallback",
|
|
807
1236
|
summary: "Callback URL for SSO provider",
|
|
@@ -986,7 +1415,7 @@ const callbackSSOSAML = (options) => {
|
|
|
986
1415
|
method: "POST",
|
|
987
1416
|
body: callbackSSOSAMLBodySchema,
|
|
988
1417
|
metadata: {
|
|
989
|
-
|
|
1418
|
+
...HIDE_METADATA,
|
|
990
1419
|
allowedMediaTypes: ["application/x-www-form-urlencoded", "application/json"],
|
|
991
1420
|
openapi: {
|
|
992
1421
|
operationId: "handleSAMLCallback",
|
|
@@ -1069,22 +1498,10 @@ const callbackSSOSAML = (options) => {
|
|
|
1069
1498
|
});
|
|
1070
1499
|
let parsedResponse;
|
|
1071
1500
|
try {
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
RelayState: RelayState || void 0
|
|
1077
|
-
} });
|
|
1078
|
-
} catch (parseError) {
|
|
1079
|
-
const nameIDMatch = decodedResponse.match(/<saml2:NameID[^>]*>([^<]+)<\/saml2:NameID>/);
|
|
1080
|
-
if (!nameIDMatch) throw parseError;
|
|
1081
|
-
parsedResponse = { extract: {
|
|
1082
|
-
nameID: nameIDMatch[1],
|
|
1083
|
-
attributes: { nameID: nameIDMatch[1] },
|
|
1084
|
-
sessionIndex: {},
|
|
1085
|
-
conditions: {}
|
|
1086
|
-
} };
|
|
1087
|
-
}
|
|
1501
|
+
parsedResponse = await sp.parseLoginResponse(idp, "post", { body: {
|
|
1502
|
+
SAMLResponse,
|
|
1503
|
+
RelayState: RelayState || void 0
|
|
1504
|
+
} });
|
|
1088
1505
|
if (!parsedResponse?.extract) throw new Error("Invalid SAML response structure");
|
|
1089
1506
|
} catch (error) {
|
|
1090
1507
|
ctx.context.logger.error("SAML response validation failed", {
|
|
@@ -1097,6 +1514,53 @@ const callbackSSOSAML = (options) => {
|
|
|
1097
1514
|
});
|
|
1098
1515
|
}
|
|
1099
1516
|
const { extract } = parsedResponse;
|
|
1517
|
+
validateSAMLTimestamp(extract.conditions, {
|
|
1518
|
+
clockSkew: options?.saml?.clockSkew,
|
|
1519
|
+
requireTimestamps: options?.saml?.requireTimestamps,
|
|
1520
|
+
logger: ctx.context.logger
|
|
1521
|
+
});
|
|
1522
|
+
const inResponseTo = extract.inResponseTo;
|
|
1523
|
+
if (options?.saml?.authnRequestStore || options?.saml?.enableInResponseToValidation) {
|
|
1524
|
+
const allowIdpInitiated = options?.saml?.allowIdpInitiated !== false;
|
|
1525
|
+
if (inResponseTo) {
|
|
1526
|
+
let storedRequest = null;
|
|
1527
|
+
if (options?.saml?.authnRequestStore) storedRequest = await options.saml.authnRequestStore.get(inResponseTo);
|
|
1528
|
+
else {
|
|
1529
|
+
const verification = await ctx.context.internalAdapter.findVerificationValue(`${AUTHN_REQUEST_KEY_PREFIX}${inResponseTo}`);
|
|
1530
|
+
if (verification) try {
|
|
1531
|
+
storedRequest = JSON.parse(verification.value);
|
|
1532
|
+
if (storedRequest && storedRequest.expiresAt < Date.now()) storedRequest = null;
|
|
1533
|
+
} catch {
|
|
1534
|
+
storedRequest = null;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
if (!storedRequest) {
|
|
1538
|
+
ctx.context.logger.error("SAML InResponseTo validation failed: unknown or expired request ID", {
|
|
1539
|
+
inResponseTo,
|
|
1540
|
+
providerId: provider.providerId
|
|
1541
|
+
});
|
|
1542
|
+
const redirectUrl = RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
|
|
1543
|
+
throw ctx.redirect(`${redirectUrl}?error=invalid_saml_response&error_description=Unknown+or+expired+request+ID`);
|
|
1544
|
+
}
|
|
1545
|
+
if (storedRequest.providerId !== provider.providerId) {
|
|
1546
|
+
ctx.context.logger.error("SAML InResponseTo validation failed: provider mismatch", {
|
|
1547
|
+
inResponseTo,
|
|
1548
|
+
expectedProvider: storedRequest.providerId,
|
|
1549
|
+
actualProvider: provider.providerId
|
|
1550
|
+
});
|
|
1551
|
+
if (options?.saml?.authnRequestStore) await options.saml.authnRequestStore.delete(inResponseTo);
|
|
1552
|
+
else await ctx.context.internalAdapter.deleteVerificationByIdentifier(`${AUTHN_REQUEST_KEY_PREFIX}${inResponseTo}`);
|
|
1553
|
+
const redirectUrl = RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
|
|
1554
|
+
throw ctx.redirect(`${redirectUrl}?error=invalid_saml_response&error_description=Provider+mismatch`);
|
|
1555
|
+
}
|
|
1556
|
+
if (options?.saml?.authnRequestStore) await options.saml.authnRequestStore.delete(inResponseTo);
|
|
1557
|
+
else await ctx.context.internalAdapter.deleteVerificationByIdentifier(`${AUTHN_REQUEST_KEY_PREFIX}${inResponseTo}`);
|
|
1558
|
+
} else if (!allowIdpInitiated) {
|
|
1559
|
+
ctx.context.logger.error("SAML IdP-initiated SSO rejected: InResponseTo missing and allowIdpInitiated is false", { providerId: provider.providerId });
|
|
1560
|
+
const redirectUrl = RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
|
|
1561
|
+
throw ctx.redirect(`${redirectUrl}?error=unsolicited_response&error_description=IdP-initiated+SSO+not+allowed`);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1100
1564
|
const attributes = extract.attributes || {};
|
|
1101
1565
|
const mapping = parsedSamlConfig.mapping ?? {};
|
|
1102
1566
|
const userInfo = {
|
|
@@ -1223,7 +1687,7 @@ const acsEndpoint = (options) => {
|
|
|
1223
1687
|
params: acsEndpointParamsSchema,
|
|
1224
1688
|
body: acsEndpointBodySchema,
|
|
1225
1689
|
metadata: {
|
|
1226
|
-
|
|
1690
|
+
...HIDE_METADATA,
|
|
1227
1691
|
allowedMediaTypes: ["application/x-www-form-urlencoded", "application/json"],
|
|
1228
1692
|
openapi: {
|
|
1229
1693
|
operationId: "handleSAMLAssertionConsumerService",
|
|
@@ -1285,26 +1749,10 @@ const acsEndpoint = (options) => {
|
|
|
1285
1749
|
}) : saml.IdentityProvider({ metadata: idpData.metadata });
|
|
1286
1750
|
let parsedResponse;
|
|
1287
1751
|
try {
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
} else if (!decodedResponse.includes("saml2:Success")) decodedResponse = decodedResponse.replace(/<saml2:StatusCode Value="[^"]+"/, "<saml2:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"");
|
|
1293
|
-
try {
|
|
1294
|
-
parsedResponse = await sp.parseLoginResponse(idp, "post", { body: {
|
|
1295
|
-
SAMLResponse,
|
|
1296
|
-
RelayState: RelayState || void 0
|
|
1297
|
-
} });
|
|
1298
|
-
} catch (parseError) {
|
|
1299
|
-
const nameIDMatch = decodedResponse.match(/<saml2:NameID[^>]*>([^<]+)<\/saml2:NameID>/);
|
|
1300
|
-
if (!nameIDMatch) throw parseError;
|
|
1301
|
-
parsedResponse = { extract: {
|
|
1302
|
-
nameID: nameIDMatch[1],
|
|
1303
|
-
attributes: { nameID: nameIDMatch[1] },
|
|
1304
|
-
sessionIndex: {},
|
|
1305
|
-
conditions: {}
|
|
1306
|
-
} };
|
|
1307
|
-
}
|
|
1752
|
+
parsedResponse = await sp.parseLoginResponse(idp, "post", { body: {
|
|
1753
|
+
SAMLResponse,
|
|
1754
|
+
RelayState: RelayState || void 0
|
|
1755
|
+
} });
|
|
1308
1756
|
if (!parsedResponse?.extract) throw new Error("Invalid SAML response structure");
|
|
1309
1757
|
} catch (error) {
|
|
1310
1758
|
ctx.context.logger.error("SAML response validation failed", {
|
|
@@ -1317,6 +1765,53 @@ const acsEndpoint = (options) => {
|
|
|
1317
1765
|
});
|
|
1318
1766
|
}
|
|
1319
1767
|
const { extract } = parsedResponse;
|
|
1768
|
+
validateSAMLTimestamp(extract.conditions, {
|
|
1769
|
+
clockSkew: options?.saml?.clockSkew,
|
|
1770
|
+
requireTimestamps: options?.saml?.requireTimestamps,
|
|
1771
|
+
logger: ctx.context.logger
|
|
1772
|
+
});
|
|
1773
|
+
const inResponseToAcs = extract.inResponseTo;
|
|
1774
|
+
if (options?.saml?.authnRequestStore || options?.saml?.enableInResponseToValidation) {
|
|
1775
|
+
const allowIdpInitiated = options?.saml?.allowIdpInitiated !== false;
|
|
1776
|
+
if (inResponseToAcs) {
|
|
1777
|
+
let storedRequest = null;
|
|
1778
|
+
if (options?.saml?.authnRequestStore) storedRequest = await options.saml.authnRequestStore.get(inResponseToAcs);
|
|
1779
|
+
else {
|
|
1780
|
+
const verification = await ctx.context.internalAdapter.findVerificationValue(`${AUTHN_REQUEST_KEY_PREFIX}${inResponseToAcs}`);
|
|
1781
|
+
if (verification) try {
|
|
1782
|
+
storedRequest = JSON.parse(verification.value);
|
|
1783
|
+
if (storedRequest && storedRequest.expiresAt < Date.now()) storedRequest = null;
|
|
1784
|
+
} catch {
|
|
1785
|
+
storedRequest = null;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
if (!storedRequest) {
|
|
1789
|
+
ctx.context.logger.error("SAML InResponseTo validation failed: unknown or expired request ID", {
|
|
1790
|
+
inResponseTo: inResponseToAcs,
|
|
1791
|
+
providerId
|
|
1792
|
+
});
|
|
1793
|
+
const redirectUrl = RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
|
|
1794
|
+
throw ctx.redirect(`${redirectUrl}?error=invalid_saml_response&error_description=Unknown+or+expired+request+ID`);
|
|
1795
|
+
}
|
|
1796
|
+
if (storedRequest.providerId !== providerId) {
|
|
1797
|
+
ctx.context.logger.error("SAML InResponseTo validation failed: provider mismatch", {
|
|
1798
|
+
inResponseTo: inResponseToAcs,
|
|
1799
|
+
expectedProvider: storedRequest.providerId,
|
|
1800
|
+
actualProvider: providerId
|
|
1801
|
+
});
|
|
1802
|
+
if (options?.saml?.authnRequestStore) await options.saml.authnRequestStore.delete(inResponseToAcs);
|
|
1803
|
+
else await ctx.context.internalAdapter.deleteVerificationByIdentifier(`${AUTHN_REQUEST_KEY_PREFIX}${inResponseToAcs}`);
|
|
1804
|
+
const redirectUrl = RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
|
|
1805
|
+
throw ctx.redirect(`${redirectUrl}?error=invalid_saml_response&error_description=Provider+mismatch`);
|
|
1806
|
+
}
|
|
1807
|
+
if (options?.saml?.authnRequestStore) await options.saml.authnRequestStore.delete(inResponseToAcs);
|
|
1808
|
+
else await ctx.context.internalAdapter.deleteVerificationByIdentifier(`${AUTHN_REQUEST_KEY_PREFIX}${inResponseToAcs}`);
|
|
1809
|
+
} else if (!allowIdpInitiated) {
|
|
1810
|
+
ctx.context.logger.error("SAML IdP-initiated SSO rejected: InResponseTo missing and allowIdpInitiated is false", { providerId });
|
|
1811
|
+
const redirectUrl = RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
|
|
1812
|
+
throw ctx.redirect(`${redirectUrl}?error=unsolicited_response&error_description=IdP-initiated+SSO+not+allowed`);
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1320
1815
|
const attributes = extract.attributes || {};
|
|
1321
1816
|
const mapping = parsedSamlConfig.mapping ?? {};
|
|
1322
1817
|
const userInfo = {
|
|
@@ -1439,18 +1934,19 @@ saml.setSchemaValidator({ async validate(xml) {
|
|
|
1439
1934
|
throw "ERR_INVALID_XML";
|
|
1440
1935
|
} });
|
|
1441
1936
|
function sso(options) {
|
|
1937
|
+
const optionsWithStore = options;
|
|
1442
1938
|
let endpoints = {
|
|
1443
1939
|
spMetadata: spMetadata(),
|
|
1444
|
-
registerSSOProvider: registerSSOProvider(
|
|
1445
|
-
signInSSO: signInSSO(
|
|
1446
|
-
callbackSSO: callbackSSO(
|
|
1447
|
-
callbackSSOSAML: callbackSSOSAML(
|
|
1448
|
-
acsEndpoint: acsEndpoint(
|
|
1940
|
+
registerSSOProvider: registerSSOProvider(optionsWithStore),
|
|
1941
|
+
signInSSO: signInSSO(optionsWithStore),
|
|
1942
|
+
callbackSSO: callbackSSO(optionsWithStore),
|
|
1943
|
+
callbackSSOSAML: callbackSSOSAML(optionsWithStore),
|
|
1944
|
+
acsEndpoint: acsEndpoint(optionsWithStore)
|
|
1449
1945
|
};
|
|
1450
1946
|
if (options?.domainVerification?.enabled) {
|
|
1451
1947
|
const domainVerificationEndpoints = {
|
|
1452
|
-
requestDomainVerification: requestDomainVerification(
|
|
1453
|
-
verifyDomain: verifyDomain(
|
|
1948
|
+
requestDomainVerification: requestDomainVerification(optionsWithStore),
|
|
1949
|
+
verifyDomain: verifyDomain(optionsWithStore)
|
|
1454
1950
|
};
|
|
1455
1951
|
endpoints = {
|
|
1456
1952
|
...endpoints,
|
|
@@ -1512,4 +2008,4 @@ function sso(options) {
|
|
|
1512
2008
|
}
|
|
1513
2009
|
|
|
1514
2010
|
//#endregion
|
|
1515
|
-
export { sso };
|
|
2011
|
+
export { DEFAULT_AUTHN_REQUEST_TTL_MS, DEFAULT_CLOCK_SKEW_MS, DiscoveryError, REQUIRED_DISCOVERY_FIELDS, computeDiscoveryUrl, createInMemoryAuthnRequestStore, discoverOIDCConfig, fetchDiscoveryDocument, needsRuntimeDiscovery, normalizeDiscoveryUrls, normalizeUrl, selectTokenEndpointAuthMethod, sso, validateDiscoveryDocument, validateDiscoveryUrl, validateSAMLTimestamp };
|