@better-auth/sso 1.4.7-beta.3 → 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/dist/index.mjs CHANGED
@@ -223,6 +223,300 @@ const verifyDomain = (options) => {
223
223
  });
224
224
  };
225
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
+
226
520
  //#endregion
227
521
  //#region src/utils.ts
228
522
  /**
@@ -254,6 +548,47 @@ const validateEmailDomain = (email, domain) => {
254
548
  //#endregion
255
549
  //#region src/routes/sso.ts
256
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
+ }
257
592
  const spMetadataQuerySchema = z.object({
258
593
  providerId: z.string(),
259
594
  format: z.enum(["xml", "json"]).default("xml")
@@ -304,6 +639,7 @@ const ssoProviderBodySchema = z.object({
304
639
  tokenEndpointAuthentication: z.enum(["client_secret_post", "client_secret_basic"]).optional(),
305
640
  jwksEndpoint: z.string({}).meta({ description: "The JWKS endpoint" }).optional(),
306
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(),
307
643
  scopes: z.array(z.string(), {}).meta({ description: "The scopes to request. Defaults to ['openid', 'email', 'profile', 'offline_access']" }).optional(),
308
644
  pkce: z.boolean({}).meta({ description: "Whether to use PKCE for the authorization flow" }).default(true).optional(),
309
645
  mapping: z.object({
@@ -571,27 +907,64 @@ const registerSSOProvider = (options) => {
571
907
  ctx.context.logger.info(`SSO provider creation attempt with existing providerId: ${body.providerId}`);
572
908
  throw new APIError("UNPROCESSABLE_ENTITY", { message: "SSO provider with this providerId already exists" });
573
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
+ };
574
961
  const provider = await ctx.context.adapter.create({
575
962
  model: "ssoProvider",
576
963
  data: {
577
964
  issuer: body.issuer,
578
965
  domain: body.domain,
579
966
  domainVerified: false,
580
- oidcConfig: body.oidcConfig ? JSON.stringify({
581
- issuer: body.issuer,
582
- clientId: body.oidcConfig.clientId,
583
- clientSecret: body.oidcConfig.clientSecret,
584
- authorizationEndpoint: body.oidcConfig.authorizationEndpoint,
585
- tokenEndpoint: body.oidcConfig.tokenEndpoint,
586
- tokenEndpointAuthentication: body.oidcConfig.tokenEndpointAuthentication,
587
- jwksEndpoint: body.oidcConfig.jwksEndpoint,
588
- pkce: body.oidcConfig.pkce,
589
- discoveryEndpoint: body.oidcConfig.discoveryEndpoint || `${body.issuer}/.well-known/openid-configuration`,
590
- mapping: body.oidcConfig.mapping,
591
- scopes: body.oidcConfig.scopes,
592
- userInfoEndpoint: body.oidcConfig.userInfoEndpoint,
593
- overrideUserInfo: ctx.body.overrideUserInfo || options?.defaultOverrideUserInfo || false
594
- }) : null,
967
+ oidcConfig: buildOIDCConfig(),
595
968
  samlConfig: body.samlConfig ? JSON.stringify({
596
969
  issuer: body.issuer,
597
970
  entryPoint: body.samlConfig.entryPoint,
@@ -1141,6 +1514,11 @@ const callbackSSOSAML = (options) => {
1141
1514
  });
1142
1515
  }
1143
1516
  const { extract } = parsedResponse;
1517
+ validateSAMLTimestamp(extract.conditions, {
1518
+ clockSkew: options?.saml?.clockSkew,
1519
+ requireTimestamps: options?.saml?.requireTimestamps,
1520
+ logger: ctx.context.logger
1521
+ });
1144
1522
  const inResponseTo = extract.inResponseTo;
1145
1523
  if (options?.saml?.authnRequestStore || options?.saml?.enableInResponseToValidation) {
1146
1524
  const allowIdpInitiated = options?.saml?.allowIdpInitiated !== false;
@@ -1151,6 +1529,7 @@ const callbackSSOSAML = (options) => {
1151
1529
  const verification = await ctx.context.internalAdapter.findVerificationValue(`${AUTHN_REQUEST_KEY_PREFIX}${inResponseTo}`);
1152
1530
  if (verification) try {
1153
1531
  storedRequest = JSON.parse(verification.value);
1532
+ if (storedRequest && storedRequest.expiresAt < Date.now()) storedRequest = null;
1154
1533
  } catch {
1155
1534
  storedRequest = null;
1156
1535
  }
@@ -1386,6 +1765,11 @@ const acsEndpoint = (options) => {
1386
1765
  });
1387
1766
  }
1388
1767
  const { extract } = parsedResponse;
1768
+ validateSAMLTimestamp(extract.conditions, {
1769
+ clockSkew: options?.saml?.clockSkew,
1770
+ requireTimestamps: options?.saml?.requireTimestamps,
1771
+ logger: ctx.context.logger
1772
+ });
1389
1773
  const inResponseToAcs = extract.inResponseTo;
1390
1774
  if (options?.saml?.authnRequestStore || options?.saml?.enableInResponseToValidation) {
1391
1775
  const allowIdpInitiated = options?.saml?.allowIdpInitiated !== false;
@@ -1396,6 +1780,7 @@ const acsEndpoint = (options) => {
1396
1780
  const verification = await ctx.context.internalAdapter.findVerificationValue(`${AUTHN_REQUEST_KEY_PREFIX}${inResponseToAcs}`);
1397
1781
  if (verification) try {
1398
1782
  storedRequest = JSON.parse(verification.value);
1783
+ if (storedRequest && storedRequest.expiresAt < Date.now()) storedRequest = null;
1399
1784
  } catch {
1400
1785
  storedRequest = null;
1401
1786
  }
@@ -1623,4 +2008,4 @@ function sso(options) {
1623
2008
  }
1624
2009
 
1625
2010
  //#endregion
1626
- export { DEFAULT_AUTHN_REQUEST_TTL_MS, createInMemoryAuthnRequestStore, 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 };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@better-auth/sso",
3
3
  "author": "Bereket Engida",
4
- "version": "1.4.7-beta.3",
4
+ "version": "1.4.7-beta.4",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
7
7
  "types": "dist/index.d.mts",
@@ -66,10 +66,10 @@
66
66
  "express": "^5.1.0",
67
67
  "oauth2-mock-server": "^8.2.0",
68
68
  "tsdown": "^0.17.2",
69
- "better-auth": "1.4.7-beta.3"
69
+ "better-auth": "1.4.7-beta.4"
70
70
  },
71
71
  "peerDependencies": {
72
- "better-auth": "1.4.7-beta.3"
72
+ "better-auth": "1.4.7-beta.4"
73
73
  },
74
74
  "scripts": {
75
75
  "test": "vitest",
package/src/index.ts CHANGED
@@ -21,12 +21,39 @@ import {
21
21
  signInSSO,
22
22
  spMetadata,
23
23
  } from "./routes/sso";
24
+
25
+ export {
26
+ DEFAULT_CLOCK_SKEW_MS,
27
+ type SAMLConditions,
28
+ type TimestampValidationOptions,
29
+ validateSAMLTimestamp,
30
+ } from "./routes/sso";
31
+
24
32
  import type { OIDCConfig, SAMLConfig, SSOOptions, SSOProvider } from "./types";
25
33
 
26
34
  export type { SAMLConfig, OIDCConfig, SSOOptions, SSOProvider };
27
35
  export type { AuthnRequestStore, AuthnRequestRecord };
28
36
  export { createInMemoryAuthnRequestStore, DEFAULT_AUTHN_REQUEST_TTL_MS };
29
37
 
38
+ export {
39
+ computeDiscoveryUrl,
40
+ type DiscoverOIDCConfigParams,
41
+ DiscoveryError,
42
+ type DiscoveryErrorCode,
43
+ discoverOIDCConfig,
44
+ fetchDiscoveryDocument,
45
+ type HydratedOIDCConfig,
46
+ needsRuntimeDiscovery,
47
+ normalizeDiscoveryUrls,
48
+ normalizeUrl,
49
+ type OIDCDiscoveryDocument,
50
+ REQUIRED_DISCOVERY_FIELDS,
51
+ type RequiredDiscoveryField,
52
+ selectTokenEndpointAuthMethod,
53
+ validateDiscoveryDocument,
54
+ validateDiscoveryUrl,
55
+ } from "./oidc";
56
+
30
57
  const fastValidator = {
31
58
  async validate(xml: string) {
32
59
  const isValid = XMLValidator.validate(xml, {