@better-auth/sso 1.8.0-beta.7 → 1.8.0-beta.9

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/client.d.mts CHANGED
@@ -1,6 +1,5 @@
1
- import { t as SSOPlugin } from "./index-BvKzNnKP.mjs";
1
+ import { t as SSOPlugin } from "./index-BkypgbRt.mjs";
2
2
  import { DBFieldAttribute } from "better-auth/db";
3
- //#region src/client.d.ts
4
3
  interface SSOClientOptions {
5
4
  domainVerification?: {
6
5
  enabled: boolean;
@@ -29,5 +28,4 @@ export declare const ssoClient: <CO extends SSOClientOptions>(options?: CO | und
29
28
  "/sso/get-provider": "GET";
30
29
  };
31
30
  };
32
- //#endregion
33
31
  export {};
package/dist/client.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { t as PACKAGE_VERSION } from "./version-B5en_Bdq.mjs";
2
- //#region src/client.ts
1
+ import { t as PACKAGE_VERSION } from "./version-D4csRLQZ.mjs";
2
+ // src/client.ts
3
3
  const ssoClient = (options) => {
4
4
  return {
5
5
  id: "sso-client",
@@ -11,5 +11,4 @@ const ssoClient = (options) => {
11
11
  }
12
12
  };
13
13
  };
14
- //#endregion
15
14
  export { ssoClient };
@@ -6,7 +6,6 @@ import { DBFieldAttribute, FieldAttributeToObject, InferAdditionalFieldsFromPlug
6
6
  import { Awaitable, DBTransactionAdapter, OAuth2Tokens, User } from "better-auth";
7
7
  import "@better-auth/core/db/adapter";
8
8
  import "@better-auth/core";
9
- //#region src/saml/algorithms.d.ts
10
9
  declare const SignatureAlgorithm: {
11
10
  readonly RSA_SHA1: "http://www.w3.org/2000/09/xmldsig#rsa-sha1";
12
11
  readonly RSA_SHA256: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
@@ -44,8 +43,6 @@ interface AlgorithmValidationOptions {
44
43
  allowedKeyEncryptionAlgorithms?: string[];
45
44
  allowedDataEncryptionAlgorithms?: string[];
46
45
  }
47
- //#endregion
48
- //#region src/types.d.ts
49
46
  interface OIDCMapping {
50
47
  email?: string | undefined;
51
48
  emailVerified?: string | undefined;
@@ -238,7 +235,13 @@ type SSOUserResolution = {
238
235
  code: string;
239
236
  message?: string | undefined;
240
237
  };
241
- /** Normalized provider attributes available to an SSO user resolver. */
238
+ /**
239
+ * Normalized provider attributes available to an SSO user resolver. When
240
+ * `mapProfileToUser` is set, `email`, `name`, and `image` hold its result,
241
+ * and `emailVerified` is `false` if it changed the email. The provider's own
242
+ * values remain in `providerClaims`, `verifiedIdTokenClaims`, or
243
+ * `providerAttributes`.
244
+ */
242
245
  type SSOProviderUserProfile = {
243
246
  email: string;
244
247
  emailVerified: boolean;
@@ -294,6 +297,68 @@ type SSOUserResolutionInput = SSOOIDCUserResolutionInput | SSOSAMLUserResolution
294
297
  interface SSOUserResolutionContext {
295
298
  database: DBTransactionAdapter;
296
299
  }
300
+ interface BaseSSOProfileMappingInput {
301
+ providerId: string;
302
+ accountKey: BaseSSOUserResolutionInput["accountKey"];
303
+ /**
304
+ * Profile fields read from the provider with the provider's `mapping`.
305
+ * `email` is absent when the provider sent no usable email.
306
+ */
307
+ providerUser: {
308
+ email?: string | undefined;
309
+ /** Always `false` unless `trustEmailVerified` is enabled. */
310
+ emailVerified: boolean;
311
+ name?: string | undefined;
312
+ image?: string | undefined;
313
+ };
314
+ }
315
+ /** OIDC profile data available to `mapProfileToUser`. */
316
+ interface SSOOIDCProfileMappingInput extends BaseSSOProfileMappingInput {
317
+ protocol: "oidc";
318
+ /** Raw claims from UserInfo, or the verified ID Token when UserInfo is absent. */
319
+ providerClaims: Record<string, unknown>;
320
+ /** Claims from the cryptographically verified ID Token, or `{}` when none was issued. */
321
+ verifiedIdTokenClaims: Record<string, unknown>;
322
+ }
323
+ /** SAML profile data available to `mapProfileToUser`. */
324
+ interface SSOSAMLProfileMappingInput extends BaseSSOProfileMappingInput {
325
+ protocol: "saml";
326
+ /**
327
+ * Attributes from the verified assertion. Multi-valued attributes remain
328
+ * arrays and all scalar values remain strings.
329
+ */
330
+ providerAttributes: Record<string, string | readonly string[]>;
331
+ /** The verified assertion's NameID, which is also `accountKey.accountId`. */
332
+ nameID: string;
333
+ /** The NameID `Format` attribute, when the assertion sets one. */
334
+ nameIDFormat?: string | undefined;
335
+ }
336
+ /** Provider profile data available to `mapProfileToUser`. */
337
+ type SSOProfileMappingInput = SSOOIDCProfileMappingInput | SSOSAMLProfileMappingInput;
338
+ /**
339
+ * User fields an application can set from `mapProfileToUser`. Omitted or
340
+ * `undefined` fields keep the provider's value. Other keys are ignored; set
341
+ * additional user fields with the provider's `mapping.extraFields` or with
342
+ * `provisionUser`.
343
+ */
344
+ interface SSOMappedUser {
345
+ /** The account subject always comes from the provider's `sub` claim or NameID. */
346
+ id?: never;
347
+ /**
348
+ * An email that differs from the provider's must be an email address and is
349
+ * stored unverified. Return `null` to refuse the email the provider sent;
350
+ * sign-in then fails with `missing_user_info`.
351
+ */
352
+ email?: string | null | undefined;
353
+ /**
354
+ * Not settable. The stored email is verified only when the provider asserts
355
+ * it with `trustEmailVerified` on. Domain verification lets an unverified
356
+ * email link to an existing user; it does not mark the email verified.
357
+ */
358
+ emailVerified?: never;
359
+ name?: string | undefined;
360
+ image?: string | undefined;
361
+ }
297
362
  interface BaseSSOProviderMutationGuardInput {
298
363
  provider: {
299
364
  id: string;
@@ -323,13 +388,33 @@ interface SSOProviderMutationGuardContext {
323
388
  database: DBTransactionAdapter;
324
389
  }
325
390
  interface SSOOptions {
391
+ /**
392
+ * Map a provider profile to user fields before the user is looked up or
393
+ * created.
394
+ *
395
+ * Runs on every OIDC and SAML sign-in, after the provider's `mapping` is
396
+ * applied. Use it for providers that send no email: sign-in fails when
397
+ * neither the provider nor this callback yields one. A SAML NameID is used
398
+ * as the email only when its `Format` is
399
+ * `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress`. If the callback
400
+ * throws or returns an invalid result, sign-in redirects with
401
+ * `SSO_PROFILE_MAPPING_FAILED`.
402
+ *
403
+ * The returned email is used to find, link, and create the user, and to
404
+ * decide whether the provider's verified domain trusts it. An email that
405
+ * differs from the provider's is stored unverified. Return only addresses
406
+ * the provider vouches for, or a placeholder built with
407
+ * `createPlaceholderEmail` from `@better-auth/core/utils/email`.
408
+ */
409
+ mapProfileToUser?: ((input: SSOProfileMappingInput) => Awaitable<SSOMappedUser>) | undefined;
326
410
  /**
327
411
  * Resolve a verified provider identity to a Better Auth user.
328
412
  *
329
413
  * For OIDC, `accountKey` is derived from the validated ID Token. For SAML,
330
414
  * it contains the verified IdP entity ID and signed NameID. Profile fields,
331
415
  * raw OIDC claims, and SAML assertion attributes are protocol-accepted
332
- * provider data and may require application-level validation.
416
+ * provider data and may require application-level validation. When
417
+ * `mapProfileToUser` is set, `providerUser` holds the mapped profile.
333
418
  *
334
419
  * The callback runs on every SSO sign-in inside the same native database
335
420
  * transaction as account finalization and session creation.
@@ -683,8 +768,6 @@ interface SSOOptions {
683
768
  idpInitiatedCallbackUrl?: string | undefined;
684
769
  };
685
770
  }
686
- //#endregion
687
- //#region src/routes/domain-verification.d.ts
688
771
  declare const requestDomainVerification: (options: SSOOptions) => import("better-call").StrictEndpoint<"/sso/request-domain-verification", {
689
772
  method: "POST";
690
773
  body: z.ZodObject<{
@@ -782,16 +865,12 @@ declare const verifyDomain: (options: SSOOptions) => import("better-call").Stric
782
865
  };
783
866
  }>>[];
784
867
  }, void>;
785
- //#endregion
786
- //#region src/utils.d.ts
787
868
  declare function parseCertificate(certPem: string): {
788
869
  fingerprintSha256: string;
789
870
  notBefore: string;
790
871
  notAfter: string;
791
872
  publicKeyAlgorithm: string;
792
873
  };
793
- //#endregion
794
- //#region src/routes/providers.d.ts
795
874
  type ParsedCert = ReturnType<typeof parseCertificate>;
796
875
  type SanitizedCert = ParsedCert | {
797
876
  error: string;
@@ -1150,8 +1229,6 @@ declare const deleteSSOProvider: (options?: SSOOptions) => import("better-call")
1150
1229
  }, {
1151
1230
  success: boolean;
1152
1231
  }>;
1153
- //#endregion
1154
- //#region src/routes/sso.d.ts
1155
1232
  declare const spMetadata: (options?: SSOOptions) => import("better-call").StrictEndpoint<"/sso/saml2/sp/metadata", {
1156
1233
  method: "GET";
1157
1234
  query: z.ZodObject<{
@@ -1200,7 +1277,9 @@ declare const registerSSOProvider: <O extends SSOOptions>(options: O) => import(
1200
1277
  }>>[];
1201
1278
  metadata: {
1202
1279
  $Infer: {
1203
- body: Record<string, any> & SSOProviderAdditionalFieldsInput<O>;
1280
+ body: {
1281
+ [x: string]: any;
1282
+ } & SSOProviderAdditionalFieldsInput<O>;
1204
1283
  };
1205
1284
  openapi: {
1206
1285
  operationId: string;
@@ -1627,8 +1706,6 @@ declare const initiateSLO: (options?: SSOOptions) => import("better-call").Stric
1627
1706
  readonly scope: "server";
1628
1707
  };
1629
1708
  }, never>;
1630
- //#endregion
1631
- //#region src/constants.d.ts
1632
1709
  /**
1633
1710
  * Default clock skew tolerance (5 minutes).
1634
1711
  * Allows for minor time differences between IdP and SP servers.
@@ -1649,8 +1726,6 @@ declare const DEFAULT_MAX_SAML_RESPONSE_SIZE: number;
1649
1726
  * Protects against oversized metadata documents.
1650
1727
  */
1651
1728
  declare const DEFAULT_MAX_SAML_METADATA_SIZE: number;
1652
- //#endregion
1653
- //#region src/routes/helpers.d.ts
1654
1729
  interface SAMLServiceProviderPolicy {
1655
1730
  /** Effective assertion-signing requirement advertised by SP metadata. */
1656
1731
  wantAssertionsSigned: boolean;
@@ -1668,8 +1743,6 @@ declare function deriveSAMLServiceProviderPolicy(config: Pick<SAMLConfig, "spMet
1668
1743
  * parsing and manual-configuration validation as SAML authentication.
1669
1744
  */
1670
1745
  declare function deriveSAMLIdentityProviderEntityID(config: SAMLConfig): string;
1671
- //#endregion
1672
- //#region src/saml/timestamp.d.ts
1673
1746
  interface TimestampValidationOptions {
1674
1747
  clockSkew?: number;
1675
1748
  requireTimestamps?: boolean;
@@ -1688,8 +1761,6 @@ interface SAMLConditions {
1688
1761
  * @throws {APIError} If timestamps are invalid, expired, or not yet valid
1689
1762
  */
1690
1763
  declare function validateSAMLTimestamp(conditions: SAMLConditions | undefined, options?: TimestampValidationOptions): void;
1691
- //#endregion
1692
- //#region src/oidc/types.d.ts
1693
1764
  /**
1694
1765
  * OIDC Discovery Types
1695
1766
  *
@@ -1856,8 +1927,6 @@ interface DiscoverOIDCConfigParams {
1856
1927
  */
1857
1928
  declare const REQUIRED_DISCOVERY_FIELDS: readonly ["issuer", "authorization_endpoint", "token_endpoint", "jwks_uri"];
1858
1929
  type RequiredDiscoveryField = (typeof REQUIRED_DISCOVERY_FIELDS)[number];
1859
- //#endregion
1860
- //#region src/oidc/discovery.d.ts
1861
1930
  /**
1862
1931
  * Main entry point: Discover and hydrate OIDC configuration from an issuer.
1863
1932
  *
@@ -1957,8 +2026,6 @@ declare function selectTokenEndpointAuthMethod(doc: OIDCDiscoveryDocument, exist
1957
2026
  * @returns true if runtime discovery should be performed
1958
2027
  */
1959
2028
  declare function needsRuntimeDiscovery(config: Partial<HydratedOIDCConfig> | undefined): boolean;
1960
- //#endregion
1961
- //#region src/index.d.ts
1962
2029
  declare module "@better-auth/core" {
1963
2030
  interface BetterAuthPluginRegistry<AuthOptions, Options> {
1964
2031
  sso: {
@@ -2022,5 +2089,4 @@ declare function sso<O extends SSOOptions>(options?: O | undefined): {
2022
2089
  };
2023
2090
  options: NoInfer<O>;
2024
2091
  };
2025
- //#endregion
2026
- export { SAMLIdentityProviderMetadata as A, SSOUserResolutionContext as B, deriveSAMLIdentityProviderEntityID as C, DEFAULT_MAX_SAML_RESPONSE_SIZE as D, DEFAULT_MAX_SAML_METADATA_SIZE as E, SSOProviderMutationGuardInput as F, DigestAlgorithm as G, AlgorithmValidationOptions as H, SSOProviderReference as I, KeyEncryptionAlgorithm as K, SSOProviderUserProfile as L, SSOOptions as M, SSOProvider as N, OIDCConfig as O, SSOProviderMutationGuardContext as P, SSOSAMLUserResolutionInput as R, SAMLServiceProviderPolicy as S, DEFAULT_CLOCK_SKEW_MS as T, DataEncryptionAlgorithm as U, SSOUserResolutionInput as V, DeprecatedAlgorithmBehavior as W, REQUIRED_DISCOVERY_FIELDS as _, fetchDiscoveryDocument as a, TimestampValidationOptions as b, normalizeUrl as c, validateDiscoveryUrl as d, DiscoverOIDCConfigParams as f, OIDCDiscoveryDocument as g, HydratedOIDCConfig as h, discoverOIDCConfig as i, SSOOIDCUserResolutionInput as j, SAMLConfig as k, selectTokenEndpointAuthMethod as l, DiscoveryErrorCode as m, sso as n, needsRuntimeDiscovery as o, DiscoveryError as p, SignatureAlgorithm as q, computeDiscoveryUrl as r, normalizeDiscoveryUrls as s, SSOPlugin as t, validateDiscoveryDocument as u, RequiredDiscoveryField as v, deriveSAMLServiceProviderPolicy as w, validateSAMLTimestamp as x, SAMLConditions as y, SSOUserResolution as z };
2092
+ export { SAMLIdentityProviderMetadata as A, SSOProviderUserProfile as B, deriveSAMLIdentityProviderEntityID as C, DEFAULT_MAX_SAML_RESPONSE_SIZE as D, DEFAULT_MAX_SAML_METADATA_SIZE as E, SSOProfileMappingInput as F, SSOUserResolutionInput as G, SSOSAMLUserResolutionInput as H, SSOProvider as I, DeprecatedAlgorithmBehavior as J, AlgorithmValidationOptions as K, SSOProviderMutationGuardContext as L, SSOOIDCProfileMappingInput as M, SSOOIDCUserResolutionInput as N, OIDCConfig as O, SSOOptions as P, SSOProviderMutationGuardInput as R, SAMLServiceProviderPolicy as S, DEFAULT_CLOCK_SKEW_MS as T, SSOUserResolution as U, SSOSAMLProfileMappingInput as V, SSOUserResolutionContext as W, KeyEncryptionAlgorithm as X, DigestAlgorithm as Y, SignatureAlgorithm as Z, REQUIRED_DISCOVERY_FIELDS as _, fetchDiscoveryDocument as a, TimestampValidationOptions as b, normalizeUrl as c, validateDiscoveryUrl as d, DiscoverOIDCConfigParams as f, OIDCDiscoveryDocument as g, HydratedOIDCConfig as h, discoverOIDCConfig as i, SSOMappedUser as j, SAMLConfig as k, selectTokenEndpointAuthMethod as l, DiscoveryErrorCode as m, sso as n, needsRuntimeDiscovery as o, DiscoveryError as p, DataEncryptionAlgorithm as q, computeDiscoveryUrl as r, normalizeDiscoveryUrls as s, SSOPlugin as t, validateDiscoveryDocument as u, RequiredDiscoveryField as v, deriveSAMLServiceProviderPolicy as w, validateSAMLTimestamp as x, SAMLConditions as y, SSOProviderReference as z };
package/dist/index.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { A as SAMLIdentityProviderMetadata, B as SSOUserResolutionContext, C as deriveSAMLIdentityProviderEntityID, D as DEFAULT_MAX_SAML_RESPONSE_SIZE, E as DEFAULT_MAX_SAML_METADATA_SIZE, F as SSOProviderMutationGuardInput, G as DigestAlgorithm, H as AlgorithmValidationOptions, I as SSOProviderReference, K as KeyEncryptionAlgorithm, L as SSOProviderUserProfile, M as SSOOptions, N as SSOProvider, O as OIDCConfig, P as SSOProviderMutationGuardContext, R as SSOSAMLUserResolutionInput, S as SAMLServiceProviderPolicy, T as DEFAULT_CLOCK_SKEW_MS, U as DataEncryptionAlgorithm, V as SSOUserResolutionInput, W as DeprecatedAlgorithmBehavior, _ as REQUIRED_DISCOVERY_FIELDS, a as fetchDiscoveryDocument, b as TimestampValidationOptions, c as normalizeUrl, d as validateDiscoveryUrl, f as DiscoverOIDCConfigParams, g as OIDCDiscoveryDocument, h as HydratedOIDCConfig, i as discoverOIDCConfig, j as SSOOIDCUserResolutionInput, k as SAMLConfig, l as selectTokenEndpointAuthMethod, m as DiscoveryErrorCode, n as sso, o as needsRuntimeDiscovery, p as DiscoveryError, q as SignatureAlgorithm, r as computeDiscoveryUrl, s as normalizeDiscoveryUrls, t as SSOPlugin, u as validateDiscoveryDocument, v as RequiredDiscoveryField, w as deriveSAMLServiceProviderPolicy, x as validateSAMLTimestamp, y as SAMLConditions, z as SSOUserResolution } from "./index-BvKzNnKP.mjs";
2
- export { type AlgorithmValidationOptions, DEFAULT_CLOCK_SKEW_MS, DEFAULT_MAX_SAML_METADATA_SIZE, DEFAULT_MAX_SAML_RESPONSE_SIZE, DataEncryptionAlgorithm, type DeprecatedAlgorithmBehavior, DigestAlgorithm, type DiscoverOIDCConfigParams, DiscoveryError, type DiscoveryErrorCode, type HydratedOIDCConfig, KeyEncryptionAlgorithm, type OIDCConfig, type OIDCDiscoveryDocument, REQUIRED_DISCOVERY_FIELDS, type RequiredDiscoveryField, type SAMLConditions, type SAMLConfig, type SAMLIdentityProviderMetadata, type SAMLServiceProviderPolicy, type SSOOIDCUserResolutionInput, type SSOOptions, SSOPlugin, type SSOProvider, type SSOProviderMutationGuardContext, type SSOProviderMutationGuardInput, type SSOProviderReference, type SSOProviderUserProfile, type SSOSAMLUserResolutionInput, type SSOUserResolution, type SSOUserResolutionContext, type SSOUserResolutionInput, SignatureAlgorithm, type TimestampValidationOptions, computeDiscoveryUrl, deriveSAMLIdentityProviderEntityID, deriveSAMLServiceProviderPolicy, discoverOIDCConfig, fetchDiscoveryDocument, needsRuntimeDiscovery, normalizeDiscoveryUrls, normalizeUrl, selectTokenEndpointAuthMethod, sso, validateDiscoveryDocument, validateDiscoveryUrl, validateSAMLTimestamp };
1
+ import { A as SAMLIdentityProviderMetadata, B as SSOProviderUserProfile, C as deriveSAMLIdentityProviderEntityID, D as DEFAULT_MAX_SAML_RESPONSE_SIZE, E as DEFAULT_MAX_SAML_METADATA_SIZE, F as SSOProfileMappingInput, G as SSOUserResolutionInput, H as SSOSAMLUserResolutionInput, I as SSOProvider, J as DeprecatedAlgorithmBehavior, K as AlgorithmValidationOptions, L as SSOProviderMutationGuardContext, M as SSOOIDCProfileMappingInput, N as SSOOIDCUserResolutionInput, O as OIDCConfig, P as SSOOptions, R as SSOProviderMutationGuardInput, S as SAMLServiceProviderPolicy, T as DEFAULT_CLOCK_SKEW_MS, U as SSOUserResolution, V as SSOSAMLProfileMappingInput, W as SSOUserResolutionContext, X as KeyEncryptionAlgorithm, Y as DigestAlgorithm, Z as SignatureAlgorithm, _ as REQUIRED_DISCOVERY_FIELDS, a as fetchDiscoveryDocument, b as TimestampValidationOptions, c as normalizeUrl, d as validateDiscoveryUrl, f as DiscoverOIDCConfigParams, g as OIDCDiscoveryDocument, h as HydratedOIDCConfig, i as discoverOIDCConfig, j as SSOMappedUser, k as SAMLConfig, l as selectTokenEndpointAuthMethod, m as DiscoveryErrorCode, n as sso, o as needsRuntimeDiscovery, p as DiscoveryError, q as DataEncryptionAlgorithm, r as computeDiscoveryUrl, s as normalizeDiscoveryUrls, t as SSOPlugin, u as validateDiscoveryDocument, v as RequiredDiscoveryField, w as deriveSAMLServiceProviderPolicy, x as validateSAMLTimestamp, y as SAMLConditions, z as SSOProviderReference } from "./index-BkypgbRt.mjs";
2
+ export { type AlgorithmValidationOptions, DEFAULT_CLOCK_SKEW_MS, DEFAULT_MAX_SAML_METADATA_SIZE, DEFAULT_MAX_SAML_RESPONSE_SIZE, DataEncryptionAlgorithm, type DeprecatedAlgorithmBehavior, DigestAlgorithm, type DiscoverOIDCConfigParams, DiscoveryError, type DiscoveryErrorCode, type HydratedOIDCConfig, KeyEncryptionAlgorithm, type OIDCConfig, type OIDCDiscoveryDocument, REQUIRED_DISCOVERY_FIELDS, type RequiredDiscoveryField, type SAMLConditions, type SAMLConfig, type SAMLIdentityProviderMetadata, type SAMLServiceProviderPolicy, type SSOMappedUser, type SSOOIDCProfileMappingInput, type SSOOIDCUserResolutionInput, type SSOOptions, SSOPlugin, type SSOProfileMappingInput, type SSOProvider, type SSOProviderMutationGuardContext, type SSOProviderMutationGuardInput, type SSOProviderReference, type SSOProviderUserProfile, type SSOSAMLProfileMappingInput, type SSOSAMLUserResolutionInput, type SSOUserResolution, type SSOUserResolutionContext, type SSOUserResolutionInput, SignatureAlgorithm, type TimestampValidationOptions, computeDiscoveryUrl, deriveSAMLIdentityProviderEntityID, deriveSAMLServiceProviderPolicy, discoverOIDCConfig, fetchDiscoveryDocument, needsRuntimeDiscovery, normalizeDiscoveryUrls, normalizeUrl, selectTokenEndpointAuthMethod, sso, validateDiscoveryDocument, validateDiscoveryUrl, validateSAMLTimestamp };
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { t as PACKAGE_VERSION } from "./version-B5en_Bdq.mjs";
2
- import { APIError, addOAuthServerContext, createAuthEndpoint, createAuthMiddleware, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
1
+ import { t as PACKAGE_VERSION } from "./version-D4csRLQZ.mjs";
2
+ import { APIError, addOAuthServerContext, createAuthEndpoint, createAuthMiddleware, generateGenericState, getSessionFromCtx, parseGenericState, sessionMiddleware } from "better-auth/api";
3
3
  import { XMLParser, XMLValidator } from "fast-xml-parser";
4
4
  import { X509Certificate } from "node:crypto";
5
5
  import { getHostname } from "tldts";
@@ -17,12 +17,13 @@ import { DOMParser } from "@xmldom/xmldom";
17
17
  import samlifyDefault, * as samlifyNamespace from "samlify";
18
18
  import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
19
19
  import { parseInputData, toZodSchema } from "better-auth/db";
20
+ import { HIDE_METADATA } from "@better-auth/core/api";
21
+ import { PRIVATE_KEY_JWT_SIGNING_ALGORITHMS, authorizationCodeRequest, createAuthorizationURL, createPrivateKeyJwtClientAssertionGetter, getOAuth2Tokens, parseStringBoolean } from "@better-auth/core/oauth2";
20
22
  import { isAPIError } from "@better-auth/core/utils/is-api-error";
21
23
  import { appendQueryParams } from "@better-auth/core/utils/url";
22
- import { HIDE_METADATA, PRIVATE_KEY_JWT_SIGNING_ALGORITHMS, authorizationCodeRequest, createAuthorizationURL, createPrivateKeyJwtClientAssertionGetter, generateGenericState, generateState, getOAuth2Tokens, parseGenericState, parseState } from "better-auth";
23
24
  import { deleteSessionCookie, setAccountCookie, setSessionCookie } from "better-auth/cookies";
24
- import { additionalAuthorizationParamsSchema, handleOAuthUserInfo } from "better-auth/oauth2";
25
- //#region src/constants.ts
25
+ import { additionalAuthorizationParamsSchema, generateState, handleOAuthUserInfo, parseState } from "better-auth/oauth2";
26
+ // src/constants.ts
26
27
  /**
27
28
  * SAML Constants
28
29
  *
@@ -61,8 +62,25 @@ const DEFAULT_MAX_SAML_RESPONSE_SIZE = 262144;
61
62
  * Protects against oversized metadata documents.
62
63
  */
63
64
  const DEFAULT_MAX_SAML_METADATA_SIZE = 102400;
64
- //#endregion
65
- //#region src/utils.ts
65
+ /**
66
+ * NameID format whose value is an email address. Other formats, such as
67
+ * persistent and transient, are opaque identifiers.
68
+ *
69
+ * @see https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf (§8.3.2)
70
+ */
71
+ const SAML_EMAIL_NAME_ID_FORMAT = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress";
72
+ /**
73
+ * Attributes read for the email, in order, when `samlConfig.mapping.email` is
74
+ * not set and neither the `email` attribute nor an `emailAddress` NameID is
75
+ * present: the claim URI many IdPs send by default, and the `mail` attribute
76
+ * by name and by OID.
77
+ */
78
+ const SAML_FALLBACK_EMAIL_ATTRIBUTES = [
79
+ "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
80
+ "mail",
81
+ "urn:oid:0.9.2342.19200300.100.1.3"
82
+ ];
83
+ // src/utils.ts
66
84
  const unsafeSAMLRedirectPathPrefix = /^\/(?:\/|\\|%2f|%5c)/i;
67
85
  const isSafeSAMLRedirectPath = (url) => url.startsWith("/") && !unsafeSAMLRedirectPathPrefix.test(url);
68
86
  /**
@@ -94,16 +112,6 @@ const domainMatches = (searchDomain, domainList) => {
94
112
  return domains.some((domain) => search === domain || search.endsWith(`.${domain}`));
95
113
  };
96
114
  /**
97
- * Strictly parse a provider-supplied email-verification claim.
98
- *
99
- * OIDC userInfo, OIDC id-token, and SAML attribute values are frequently
100
- * strings, so a loose `Boolean(value)` or truthy fallback treats the string
101
- * `"false"` as verified. Only a boolean `true` or the exact string `"true"`
102
- * count as verified; every other value, including `"false"`, `"0"`, `""`,
103
- * numbers, arrays, and objects, is unverified.
104
- */
105
- const parseProviderEmailVerified = (value) => value === true || value === "true";
106
- /**
107
115
  * Validates email domain against allowed domain(s).
108
116
  * Supports comma-separated domains for multi-domain SSO.
109
117
  */
@@ -151,8 +159,7 @@ function maskClientId(clientId) {
151
159
  if (clientId.length <= 4) return "****";
152
160
  return `****${clientId.slice(-4)}`;
153
161
  }
154
- //#endregion
155
- //#region src/linking/org-assignment.ts
162
+ // src/linking/org-assignment.ts
156
163
  const VERIFIED_PROVIDER_PAGE_SIZE = 100;
157
164
  function getEmailDomain(email) {
158
165
  const parts = email.trim().toLowerCase().split("@");
@@ -295,8 +302,7 @@ async function assignOrganizationByDomain(ctx, options) {
295
302
  ...options
296
303
  });
297
304
  }
298
- //#endregion
299
- //#region src/oidc/types.ts
305
+ // src/oidc/types.ts
300
306
  /**
301
307
  * Custom error class for OIDC discovery failures.
302
308
  * Can be caught and mapped to APIError at the edge.
@@ -321,8 +327,7 @@ const REQUIRED_DISCOVERY_FIELDS = [
321
327
  "token_endpoint",
322
328
  "jwks_uri"
323
329
  ];
324
- //#endregion
325
- //#region src/oidc/discovery.ts
330
+ // src/oidc/discovery.ts
326
331
  /**
327
332
  * OIDC Discovery Pipeline
328
333
  *
@@ -788,8 +793,7 @@ async function ensureRuntimeDiscovery(config, issuer, isTrustedOrigin) {
788
793
  await assertServerFetchedOIDCEndpointsAllowed(resolved, isTrustedOrigin);
789
794
  return resolved;
790
795
  }
791
- //#endregion
792
- //#region src/oidc/errors.ts
796
+ // src/oidc/errors.ts
793
797
  /**
794
798
  * OIDC Discovery Error Mapping
795
799
  *
@@ -869,8 +873,7 @@ function mapDiscoveryErrorToAPIError(error) {
869
873
  });
870
874
  }
871
875
  }
872
- //#endregion
873
- //#region src/provider-reference.ts
876
+ // src/provider-reference.ts
874
877
  const SSO_PROVIDER_STATE_KEY = "ssoProviderReference";
875
878
  function serializeCanonical(value) {
876
879
  if (value === null) return "null";
@@ -965,8 +968,7 @@ async function isCurrentSSOProviderReference(provider, reference) {
965
968
  if (source.type === "persisted" && (reference.source.type !== "persisted" || source.recordId !== reference.source.recordId)) return false;
966
969
  return reference.authenticationConfigurationFingerprint === await computeProviderAuthenticationFingerprint(provider);
967
970
  }
968
- //#endregion
969
- //#region src/saml/parser.ts
971
+ // src/saml/parser.ts
970
972
  const xmlParser = new XMLParser({
971
973
  ignoreAttributes: false,
972
974
  attributeNamePrefix: "@_",
@@ -999,8 +1001,7 @@ function countAllNodes(obj, nodeName) {
999
1001
  else if (typeof value === "object" && value !== null) count += countAllNodes(value, nodeName);
1000
1002
  return count;
1001
1003
  }
1002
- //#endregion
1003
- //#region src/saml/algorithms.ts
1004
+ // src/saml/algorithms.ts
1004
1005
  const SignatureAlgorithm = {
1005
1006
  RSA_SHA1: "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
1006
1007
  RSA_SHA256: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
@@ -1177,8 +1178,7 @@ function validateConfigAlgorithms(config, options = {}) {
1177
1178
  }
1178
1179
  }
1179
1180
  const saml = typeof samlifyNamespace.SPMetadata === "function" && typeof samlifyNamespace.setSchemaValidator === "function" ? samlifyNamespace : samlifyDefault ?? samlifyNamespace;
1180
- //#endregion
1181
- //#region src/saml/assertions.ts
1181
+ // src/saml/assertions.ts
1182
1182
  const SAML_ASSERTION_NAMESPACE = "urn:oasis:names:tc:SAML:2.0:assertion";
1183
1183
  const SAML_PROTOCOL_NAMESPACE = "urn:oasis:names:tc:SAML:2.0:protocol";
1184
1184
  const XML_SIGNATURE_NAMESPACE = "http://www.w3.org/2000/09/xmldsig#";
@@ -1254,8 +1254,7 @@ function verifySAMLAssertionSignature(samlContent, verificationOptions) {
1254
1254
  throw assertionSignatureFailure();
1255
1255
  }
1256
1256
  }
1257
- //#endregion
1258
- //#region src/saml/error-codes.ts
1257
+ // src/saml/error-codes.ts
1259
1258
  const SAML_ERROR_CODES = defineErrorCodes({
1260
1259
  SINGLE_LOGOUT_NOT_ENABLED: "Single Logout is not enabled",
1261
1260
  INVALID_LOGOUT_RESPONSE: "Invalid LogoutResponse",
@@ -1265,8 +1264,7 @@ const SAML_ERROR_CODES = defineErrorCodes({
1265
1264
  SAML_PROVIDER_NOT_FOUND: "SAML provider not found",
1266
1265
  CERT_SOURCE_MISSING: "samlConfig requires either a signing certificate (cert or idpMetadata.cert) or an idpMetadata.metadata XML document."
1267
1266
  });
1268
- //#endregion
1269
- //#region src/saml/cert.ts
1267
+ // src/saml/cert.ts
1270
1268
  /**
1271
1269
  * IdP signing-certificate rules for SAML configs. Centralized so the runtime
1272
1270
  * verification path (`createIdP`), the sanitizer (`getSSOProvider` and
@@ -1295,8 +1293,7 @@ function validateCertSources(config) {
1295
1293
  const hasExplicitCert = config.idpMetadata?.cert !== void 0 || config.cert !== void 0;
1296
1294
  if (!hasMetadataXml && !hasExplicitCert) throw APIError.from("BAD_REQUEST", SAML_ERROR_CODES.CERT_SOURCE_MISSING);
1297
1295
  }
1298
- //#endregion
1299
- //#region src/saml/response-binding.ts
1296
+ // src/saml/response-binding.ts
1300
1297
  const SAML_HTTP_POST_BINDING = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST";
1301
1298
  const SAML_BEARER_CONFIRMATION_METHOD = "urn:oasis:names:tc:SAML:2.0:cm:bearer";
1302
1299
  const SAML_METADATA_NAMESPACE = "urn:oasis:names:tc:SAML:2.0:metadata";
@@ -1383,7 +1380,7 @@ function parseSAMLServiceProviderMetadata(metadata) {
1383
1380
  if (value) nameIDFormats.push(value);
1384
1381
  }
1385
1382
  for (const service of directMetadataChildren(descriptor, "AssertionConsumerService")) {
1386
- if (service.getAttribute("Binding") !== "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST") continue;
1383
+ if (service.getAttribute("Binding") !== SAML_HTTP_POST_BINDING) continue;
1387
1384
  const location = service.getAttribute("Location")?.trim();
1388
1385
  if (!location || !isAbsoluteHttpUrl(location)) throw new Error("Invalid SAML POST AssertionConsumerService");
1389
1386
  postAssertionConsumerServiceUrls.push(location);
@@ -1436,6 +1433,15 @@ function validateAudienceRestrictions(assertion, expectedAudiences) {
1436
1433
  code: "SAML_AUDIENCE_MISMATCH"
1437
1434
  });
1438
1435
  }
1436
+ /**
1437
+ * Reads the user's email from a SAML assertion: the mapped attribute (default
1438
+ * `email`), then a NameID whose Format is `emailAddress`, then, without a
1439
+ * mapped attribute, the fallback email attributes. Other NameIDs are opaque or
1440
+ * unspecified, so they never become the email.
1441
+ */
1442
+ function readSAMLEmail(attr, options) {
1443
+ return attr(options.mappedAttribute || "email") || (options.nameIDFormat === "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" ? options.nameID : void 0) || (options.mappedAttribute ? void 0 : SAML_FALLBACK_EMAIL_ATTRIBUTES.map(attr).find(Boolean));
1444
+ }
1439
1445
  function getBearerSubjectConfirmationData(assertion) {
1440
1446
  return toNodeArray(firstNode(assertion.Subject)?.SubjectConfirmation).filter((confirmation) => confirmation["@_Method"] === SAML_BEARER_CONFIRMATION_METHOD).map((confirmation) => firstNode(confirmation.SubjectConfirmationData)).filter((data) => !!data);
1441
1447
  }
@@ -1463,6 +1469,11 @@ function validateResponseDestination(response, expectedRecipients) {
1463
1469
  code: "SAML_DESTINATION_MISMATCH"
1464
1470
  });
1465
1471
  }
1472
+ /**
1473
+ * Validates that the assertion is bound to this Service Provider, then returns
1474
+ * the NameID format so user attribute extraction need not parse the response
1475
+ * again.
1476
+ */
1466
1477
  function validateSAMLResponseBinding(samlContent, options) {
1467
1478
  const expectedAudiences = toStringSet(options.expectedAudiences);
1468
1479
  const expectedRecipients = toStringSet(options.expectedRecipients);
@@ -1472,9 +1483,10 @@ function validateSAMLResponseBinding(samlContent, options) {
1472
1483
  validateAudienceRestrictions(assertion, expectedAudiences);
1473
1484
  validateBearerRecipient(assertion, expectedRecipients);
1474
1485
  validateResponseDestination(response, expectedRecipients);
1486
+ const nameIDFormat = firstNode(firstNode(assertion.Subject)?.NameID)?.["@_Format"];
1487
+ return { nameIDFormat: typeof nameIDFormat === "string" && nameIDFormat ? nameIDFormat : void 0 };
1475
1488
  }
1476
- //#endregion
1477
- //#region src/saml/response-validation.ts
1489
+ // src/saml/response-validation.ts
1478
1490
  function errorRedirectUrl(base, error, description) {
1479
1491
  try {
1480
1492
  const url = new URL(base);
@@ -1571,9 +1583,66 @@ function validateAudience(c, ctx) {
1571
1583
  throw c.redirect(errorRedirectUrl(ctx.redirectUrl, "invalid_saml_response", "Audience mismatch"));
1572
1584
  }
1573
1585
  }
1574
- //#endregion
1575
- //#region src/user-resolution.ts
1586
+ // src/user-resolution.ts
1576
1587
  const SSO_AUTHENTICATION_FAILURE = Symbol("SSO_AUTHENTICATION_FAILURE");
1588
+ /**
1589
+ * Applies `mapProfileToUser` to the profile read from the provider. Only the
1590
+ * documented user fields are taken from the callback's result.
1591
+ */
1592
+ async function mapSSOProfile(options, input, logger) {
1593
+ if (!options?.mapProfileToUser) return input.providerUser;
1594
+ let mappedUser;
1595
+ try {
1596
+ const result = await options.mapProfileToUser(structuredClone(input));
1597
+ if (isRecord$1(result)) {
1598
+ const { email, name, image } = result;
1599
+ mappedUser = {
1600
+ email,
1601
+ name,
1602
+ image
1603
+ };
1604
+ }
1605
+ } catch (error) {
1606
+ logFailure(logger, "SSO profile mapping failed", {
1607
+ providerId: input.providerId,
1608
+ protocol: input.protocol,
1609
+ error
1610
+ });
1611
+ throw profileMappingFailure();
1612
+ }
1613
+ if (!isSSOMappedUser(mappedUser) || !isAcceptedEmail(mappedUser.email, input.providerUser.email)) {
1614
+ logFailure(logger, "SSO profile mapper returned an invalid profile", {
1615
+ providerId: input.providerId,
1616
+ protocol: input.protocol
1617
+ });
1618
+ throw profileMappingFailure();
1619
+ }
1620
+ const { email, name, image } = mappedUser;
1621
+ const providerUser = input.providerUser;
1622
+ const mappedEmail = email === null ? void 0 : email ?? providerUser.email;
1623
+ return {
1624
+ email: mappedEmail,
1625
+ emailVerified: providerUser.emailVerified && mappedEmail?.toLowerCase() === providerUser.email?.toLowerCase(),
1626
+ name: name ?? providerUser.name,
1627
+ image: image ?? providerUser.image
1628
+ };
1629
+ }
1630
+ function isOptionalString(value) {
1631
+ return value === void 0 || typeof value === "string";
1632
+ }
1633
+ function isSSOMappedUser(value) {
1634
+ return isRecord$1(value) && (value.email === null || isOptionalString(value.email)) && isOptionalString(value.name) && isOptionalString(value.image);
1635
+ }
1636
+ /** A changed email must be an address; the provider's own email passes as sent. */
1637
+ function isAcceptedEmail(email, providerEmail) {
1638
+ return typeof email !== "string" || email.toLowerCase() === providerEmail?.toLowerCase() || z.email().safeParse(email).success;
1639
+ }
1640
+ function profileMappingFailure() {
1641
+ return new APIError("INTERNAL_SERVER_ERROR", {
1642
+ code: "SSO_PROFILE_MAPPING_FAILED",
1643
+ message: "Unable to map the SSO profile"
1644
+ });
1645
+ }
1577
1646
  function isRecord$1(value) {
1578
1647
  return typeof value === "object" && value !== null && !Array.isArray(value);
1579
1648
  }
@@ -1586,9 +1655,9 @@ function isSSOUserResolution(value) {
1586
1655
  if (value.action === "link") return isNonEmptyString(value.userId) && (value.profile === "preserve" || value.profile === "update");
1587
1656
  return value.action === "reject" && isNonEmptyString(value.code) && (value.message === void 0 || typeof value.message === "string");
1588
1657
  }
1589
- function logFailure(logger, message) {
1658
+ function logFailure(logger, message, details) {
1590
1659
  try {
1591
- logger.error(message);
1660
+ logger.error(message, ...details ? [details] : []);
1592
1661
  } catch {}
1593
1662
  }
1594
1663
  function resolutionFailure() {
@@ -1658,8 +1727,7 @@ function getFailedSSOAuthenticationResult(error) {
1658
1727
  if (!(error instanceof Error) || !(SSO_AUTHENTICATION_FAILURE in error) || error[SSO_AUTHENTICATION_FAILURE] !== true || !("result" in error)) return;
1659
1728
  return error.result;
1660
1729
  }
1661
- //#endregion
1662
- //#region src/routes/helpers.ts
1730
+ // src/routes/helpers.ts
1663
1731
  /**
1664
1732
  * Same as `normalizePem`, but applied across the resolved list of IdP signing
1665
1733
  * certificates so multi-cert rotation configs survive the line-trim step.
@@ -1693,6 +1761,7 @@ async function findSAMLProvider(providerId, options, adapter) {
1693
1761
  }
1694
1762
  function createSP(config, baseURL, providerId, opts) {
1695
1763
  const spData = config.spMetadata;
1764
+ const clockSkew = opts?.clockSkew ?? 3e5;
1696
1765
  const sloLocation = `${baseURL}/sso/saml2/sp/slo/${providerId}`;
1697
1766
  const acsUrl = `${baseURL}/sso/saml2/sp/acs/${providerId}`;
1698
1767
  let metadata = spData?.metadata;
@@ -1725,7 +1794,7 @@ function createSP(config, baseURL, providerId, opts) {
1725
1794
  encPrivateKey: normalizePem(spData?.encPrivateKey),
1726
1795
  encPrivateKeyPass: spData?.encPrivateKeyPass,
1727
1796
  relayState: opts?.relayState,
1728
- clockDrifts: opts?.clockSkew && opts?.clockSkew !== 0 ? [-opts.clockSkew, opts.clockSkew] : void 0
1797
+ clockDrifts: [-clockSkew, clockSkew]
1729
1798
  });
1730
1799
  }
1731
1800
  /**
@@ -1822,8 +1891,7 @@ function createSAMLPostForm(action, samlParam, samlValue, relayState) {
1822
1891
  const html = `<!DOCTYPE html><html><body onload="document.forms[0].submit();"><form method="POST" action="${safeAction}"><input type="hidden" name="${safeSamlParam}" value="${safeSamlValue}" />${safeRelayState ? `<input type="hidden" name="RelayState" value="${safeRelayState}" />` : ""}<noscript><input type="submit" value="Continue" /></noscript></form></body></html>`;
1823
1892
  return new Response(html, { headers: { "Content-Type": "text/html" } });
1824
1893
  }
1825
- //#endregion
1826
- //#region src/routes/schemas.ts
1894
+ // src/routes/schemas.ts
1827
1895
  const absoluteUrlSchema = z.url();
1828
1896
  const samlRedirectUrlSchema = z.string().refine((url) => isSafeSAMLRedirectPath(url) || absoluteUrlSchema.safeParse(url).success, { message: "Expected an absolute URL or a relative path starting with /" });
1829
1897
  function getSSOProviderAdditionalFields$1(options) {
@@ -1966,8 +2034,7 @@ function getUpdateSSOProviderBodySchema(options) {
1966
2034
  ...getSSOProviderAdditionalFieldsSchema(options).partial().shape
1967
2035
  });
1968
2036
  }
1969
- //#endregion
1970
- //#region src/routes/providers.ts
2037
+ // src/routes/providers.ts
1971
2038
  const ADMIN_ROLES = ["owner", "admin"];
1972
2039
  const OIDC_IDENTITY_BOUNDARY_FIELDS = [
1973
2040
  "authorizationEndpoint",
@@ -2518,8 +2585,7 @@ const deleteSSOProvider = (options) => {
2518
2585
  return ctx.json({ success: true });
2519
2586
  });
2520
2587
  };
2521
- //#endregion
2522
- //#region src/routes/domain-verification.ts
2588
+ // src/routes/domain-verification.ts
2523
2589
  const DNS_LABEL_MAX_LENGTH = 63;
2524
2590
  const DEFAULT_TOKEN_PREFIX = "better-auth-token";
2525
2591
  const domainVerificationBodySchema = z.object({ providerId: z.string() });
@@ -2648,8 +2714,7 @@ const verifyDomain = (options) => {
2648
2714
  ctx.setStatus(204);
2649
2715
  });
2650
2716
  };
2651
- //#endregion
2652
- //#region src/saml-state.ts
2717
+ // src/saml-state.ts
2653
2718
  async function generateRelayState(c, link, providerReference) {
2654
2719
  const callbackURL = c.body.callbackURL;
2655
2720
  if (!callbackURL) throw new APIError("BAD_REQUEST", { message: "callbackURL is required" });
@@ -2699,8 +2764,7 @@ async function parseRelayState(c) {
2699
2764
  if (!parsedData.errorURL) parsedData.errorURL = errorURL;
2700
2765
  return parsedData;
2701
2766
  }
2702
- //#endregion
2703
- //#region src/saml/timestamp.ts
2767
+ // src/saml/timestamp.ts
2704
2768
  /**
2705
2769
  * Validates SAML assertion timestamp conditions (NotBefore/NotOnOrAfter).
2706
2770
  * Prevents acceptance of expired or future-dated assertions.
@@ -2740,8 +2804,7 @@ function validateSAMLTimestamp(conditions, options = {}) {
2740
2804
  });
2741
2805
  }
2742
2806
  }
2743
- //#endregion
2744
- //#region src/routes/saml-pipeline.ts
2807
+ // src/routes/saml-pipeline.ts
2745
2808
  function isSameSSOProviderReference(left, right) {
2746
2809
  if (left.providerId !== right.providerId || left.authenticationConfigurationFingerprint !== right.authenticationConfigurationFingerprint || left.source.type !== right.source.type) return false;
2747
2810
  return left.source.type === "configured" ? true : right.source.type === "persisted" && left.source.recordId === right.source.recordId;
@@ -2806,17 +2869,12 @@ function getSAMLRedirectCandidates(relayStateCallbackUrl, samlConfig, samlOption
2806
2869
  samlConfig?.callbackUrl
2807
2870
  ];
2808
2871
  }
2809
- function toArray(value) {
2810
- if (Array.isArray(value)) return value;
2811
- return value ? [value] : [];
2812
- }
2813
- function getExpectedSAMLRecipients(config, baseURL, providerId, currentCallbackPath, assertionConsumerServiceUrl) {
2872
+ function getExpectedSAMLRecipients(config, baseURL, providerId, currentCallbackPath) {
2814
2873
  const configuredPostAssertionConsumerServiceUrls = getSAMLPostAssertionConsumerServiceUrls(config.spMetadata?.metadata);
2815
2874
  return [
2816
2875
  currentCallbackPath,
2817
2876
  `${baseURL}/sso/saml2/sp/acs/${providerId}`,
2818
- ...configuredPostAssertionConsumerServiceUrls,
2819
- ...toArray(assertionConsumerServiceUrl)
2877
+ ...configuredPostAssertionConsumerServiceUrls
2820
2878
  ];
2821
2879
  }
2822
2880
  async function getSAMLResponseBindingContent(sp, samlContent) {
@@ -2827,20 +2885,16 @@ async function getSAMLResponseBindingContent(sp, samlContent) {
2827
2885
  /**
2828
2886
  * Extracts the Assertion ID from a SAML response XML.
2829
2887
  * Used for replay protection per SAML 2.0 Core section 2.3.3.
2888
+ *
2889
+ * Reads the ID through the DOM so character references are decoded the same
2890
+ * way the signature check decodes them: re-spelling the ID keeps the
2891
+ * signature valid, so it must not change the replay key.
2830
2892
  */
2831
2893
  function extractAssertionId(samlContent) {
2832
2894
  try {
2833
- const parsed = new XMLParser({
2834
- ignoreAttributes: false,
2835
- attributeNamePrefix: "@_",
2836
- removeNSPrefix: true
2837
- }).parse(samlContent);
2838
- const response = parsed.Response || parsed["samlp:Response"];
2839
- if (!response) return null;
2840
- const rawAssertion = response.Assertion || response["saml:Assertion"];
2841
- const assertion = Array.isArray(rawAssertion) ? rawAssertion[0] : rawAssertion;
2842
- if (!assertion) return null;
2843
- return assertion["@_ID"] || null;
2895
+ const response = new DOMParser().parseFromString(samlContent, "text/xml").documentElement;
2896
+ if (response?.localName !== "Response") return null;
2897
+ return Array.from(response.childNodes).find((node) => node.nodeType === 1 && node.localName === "Assertion")?.getAttribute("ID") || null;
2844
2898
  } catch {
2845
2899
  return null;
2846
2900
  }
@@ -2877,6 +2931,12 @@ async function processSAMLResponse(ctx, params, options) {
2877
2931
  const samlRedirectUrl = getSafeRedirectUrl(redirectCandidates, currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
2878
2932
  const samlErrorRedirectUrl = getSafeRedirectUrl([relayState?.errorURL, samlRedirectUrl], currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
2879
2933
  params.onErrorRedirectResolved?.(samlErrorRedirectUrl);
2934
+ const redirectOnCodedAPIError = (e) => {
2935
+ if (isAPIError(e) && e.body?.code) throw ctx.redirect(buildSAMLRedirectUrl(samlErrorRedirectUrl, {
2936
+ error: e.body.code,
2937
+ ...e.body.message ? { error_description: e.body.message } : {}
2938
+ }));
2939
+ };
2880
2940
  const stateProviderReference = parseSSOProviderReference(relayState?.serverContext?.[SSO_PROVIDER_STATE_KEY]);
2881
2941
  validateSingleAssertion(SAMLResponse);
2882
2942
  let parsedResponse;
@@ -2899,17 +2959,17 @@ async function processSAMLResponse(ctx, params, options) {
2899
2959
  logger: ctx.context.logger
2900
2960
  });
2901
2961
  const expectedAudiences = [sp.entityMeta.getEntityID(), parsedSamlConfig.audience];
2902
- const assertionConsumerServiceUrl = sp.entityMeta.getAssertionConsumerService(SAML_HTTP_POST_BINDING);
2903
- const expectedRecipients = getExpectedSAMLRecipients(parsedSamlConfig, ctx.context.baseURL, providerId, currentCallbackPath, assertionConsumerServiceUrl);
2962
+ const expectedRecipients = getExpectedSAMLRecipients(parsedSamlConfig, ctx.context.baseURL, providerId, currentCallbackPath);
2904
2963
  const serviceProviderPolicy = deriveSAMLServiceProviderPolicy(parsedSamlConfig);
2905
2964
  let samlBindingContent;
2965
+ let boundAssertion;
2906
2966
  try {
2907
2967
  samlBindingContent = await getSAMLResponseBindingContent(sp, samlContent);
2908
2968
  if (serviceProviderPolicy.wantAssertionsSigned) verifySAMLAssertionSignature(samlBindingContent, {
2909
2969
  metadata: idp.entityMeta,
2910
2970
  signatureAlgorithm: idp.entitySetting.requestSignatureAlgorithm
2911
2971
  });
2912
- validateSAMLResponseBinding(samlBindingContent, {
2972
+ boundAssertion = validateSAMLResponseBinding(samlBindingContent, {
2913
2973
  expectedAudiences,
2914
2974
  expectedRecipients
2915
2975
  });
@@ -2995,6 +3055,7 @@ async function processSAMLResponse(ctx, params, options) {
2995
3055
  for (const [name, value] of Object.entries(attributes)) if (typeof value === "string") providerAttributes[name] = value;
2996
3056
  else if (Array.isArray(value) && value.every((entry) => typeof entry === "string")) providerAttributes[name] = value;
2997
3057
  const mapping = parsedSamlConfig.mapping ?? {};
3058
+ const { nameIDFormat } = boundAssertion;
2998
3059
  const attr = (key) => {
2999
3060
  const value = attributes[key];
3000
3061
  return Array.isArray(value) ? value[0] : value;
@@ -3002,34 +3063,63 @@ async function processSAMLResponse(ctx, params, options) {
3002
3063
  const userInfo = {
3003
3064
  ...Object.fromEntries(Object.entries(mapping.extraFields || {}).map(([key, value]) => [key, attributes[value]])),
3004
3065
  id: extract.nameID,
3005
- email: (attr(mapping.email || "email") || extract.nameID || "").toLowerCase(),
3006
- name: [attr(mapping.firstName || "givenName"), attr(mapping.lastName || "surname")].filter(Boolean).join(" ") || attr(mapping.name || "displayName") || extract.nameID,
3007
- emailVerified: options?.trustEmailVerified && mapping.emailVerified ? parseProviderEmailVerified(attr(mapping.emailVerified)) : false
3066
+ email: readSAMLEmail(attr, {
3067
+ mappedAttribute: mapping.email,
3068
+ nameID: extract.nameID,
3069
+ nameIDFormat
3070
+ }),
3071
+ name: [attr(mapping.firstName || "givenName"), attr(mapping.lastName || "surname")].filter(Boolean).join(" ") || attr(mapping.name || "displayName") || void 0,
3072
+ emailVerified: options?.trustEmailVerified && mapping.emailVerified ? parseStringBoolean(attr(mapping.emailVerified)) ?? false : false
3008
3073
  };
3009
- if (!userInfo.id || !userInfo.email) {
3010
- ctx.context.logger.error("Missing essential user info from SAML response", {
3074
+ const providerUserAttributes = Object.fromEntries(Object.entries(userInfo).filter(([key]) => key !== "id"));
3075
+ const missingUserInfo = (hasNameId, providerSentEmail = false) => {
3076
+ ctx.context.logger.error(providerSentEmail ? "mapProfileToUser refused the email the SAML provider sent" : hasNameId ? `SAML sign-in has no email. Send an email attribute, or a NameID with Format ${SAML_EMAIL_NAME_ID_FORMAT}, from the IdP; set samlConfig.mapping.email; or return an email from mapProfileToUser.` : "SAML response has no NameID", {
3011
3077
  providerId,
3012
3078
  attributeNames: Object.keys(attributes),
3013
- hasNameId: Boolean(userInfo.id),
3014
- hasEmail: Boolean(userInfo.email)
3079
+ nameIDFormat
3080
+ });
3081
+ return new APIError("BAD_REQUEST", {
3082
+ code: "MISSING_USER_INFO",
3083
+ message: "Unable to extract user ID or email from SAML response. Map an email attribute, or create a placeholder with `mapProfileToUser`."
3015
3084
  });
3016
- throw new APIError("BAD_REQUEST", { message: "Unable to extract user ID or email from SAML response" });
3017
- }
3018
- const providerUserAttributes = Object.fromEntries(Object.entries(userInfo).filter(([key]) => key !== "id"));
3019
- const providerUser = {
3020
- ...providerUserAttributes,
3021
- email: userInfo.email,
3022
- name: userInfo.name || userInfo.email,
3023
- image: typeof providerUserAttributes.image === "string" ? providerUserAttributes.image : void 0,
3024
- emailVerified: userInfo.emailVerified
3025
3085
  };
3086
+ if (!userInfo.id) throw missingUserInfo(false);
3026
3087
  const accountKey = {
3027
3088
  issuer,
3028
3089
  accountId: userInfo.id
3029
3090
  };
3030
- const isTrustedProvider = "domainVerified" in provider && !!provider.domainVerified && validateEmailDomain(userInfo.email, provider.domain);
3091
+ const mappedUser = await mapSSOProfile(options, {
3092
+ protocol: "saml",
3093
+ providerId,
3094
+ accountKey,
3095
+ providerUser: {
3096
+ email: userInfo.email,
3097
+ emailVerified: userInfo.emailVerified,
3098
+ name: userInfo.name,
3099
+ image: typeof providerUserAttributes.image === "string" ? providerUserAttributes.image : void 0
3100
+ },
3101
+ providerAttributes,
3102
+ nameID: userInfo.id,
3103
+ nameIDFormat
3104
+ }, ctx.context.logger).catch((e) => {
3105
+ redirectOnCodedAPIError(e);
3106
+ throw e;
3107
+ });
3108
+ const email = mappedUser.email?.toLowerCase();
3109
+ if (!email) throw missingUserInfo(true, Boolean(userInfo.email));
3110
+ const providerUser = {
3111
+ ...providerUserAttributes,
3112
+ email,
3113
+ name: mappedUser.name || email,
3114
+ image: mappedUser.image,
3115
+ emailVerified: mappedUser.emailVerified
3116
+ };
3117
+ const mappedUserInfo = {
3118
+ ...providerUser,
3119
+ id: accountKey.accountId
3120
+ };
3121
+ const isTrustedProvider = "domainVerified" in provider && !!provider.domainVerified && validateEmailDomain(email, provider.domain);
3031
3122
  const callbackUrl = redirectCandidates.some(Boolean) ? samlRedirectUrl : ctx.context.baseURL;
3032
- const errorUrl = samlErrorRedirectUrl;
3033
3123
  let result;
3034
3124
  try {
3035
3125
  if (options?.resolveUser) {
@@ -3057,10 +3147,7 @@ async function processSAMLResponse(ctx, params, options) {
3057
3147
  ...resolution.message === void 0 ? {} : { message: resolution.message }
3058
3148
  });
3059
3149
  const authentication = await handleOAuthUserInfo(ctx, {
3060
- userInfo: {
3061
- ...providerUser,
3062
- id: userInfo.id
3063
- },
3150
+ userInfo: mappedUserInfo,
3064
3151
  account: {
3065
3152
  providerId,
3066
3153
  accountId: accountKey.accountId,
@@ -3092,17 +3179,16 @@ async function processSAMLResponse(ctx, params, options) {
3092
3179
  } catch (e) {
3093
3180
  const failedAuthentication = getFailedSSOAuthenticationResult(e);
3094
3181
  if (failedAuthentication) result = failedAuthentication;
3095
- else if (isAPIError(e) && e.body?.code) throw ctx.redirect(buildSAMLRedirectUrl(errorUrl, {
3096
- error: e.body.code,
3097
- ...e.body.message ? { error_description: e.body.message } : {}
3098
- }));
3099
- else throw e;
3182
+ else {
3183
+ redirectOnCodedAPIError(e);
3184
+ throw e;
3185
+ }
3100
3186
  }
3101
3187
  if (result.error) throw ctx.redirect(buildSAMLRedirectUrl(callbackUrl, { error: result.error.split(" ").join("_") }));
3102
3188
  const { session, user } = result.data;
3103
3189
  if (options?.provisionUser && (result.isRegister || options.provisionUserOnEveryLogin)) await options.provisionUser({
3104
3190
  user,
3105
- userInfo,
3191
+ userInfo: mappedUserInfo,
3106
3192
  provider
3107
3193
  });
3108
3194
  await assignOrganizationFromProvider(ctx, {
@@ -3110,9 +3196,9 @@ async function processSAMLResponse(ctx, params, options) {
3110
3196
  profile: {
3111
3197
  providerType: "saml",
3112
3198
  providerId,
3113
- accountId: userInfo.id,
3114
- email: userInfo.email,
3115
- emailVerified: userInfo.emailVerified,
3199
+ accountId: accountKey.accountId,
3200
+ email,
3201
+ emailVerified: providerUser.emailVerified,
3116
3202
  rawAttributes: attributes
3117
3203
  },
3118
3204
  provider,
@@ -3145,8 +3231,7 @@ async function processSAMLResponse(ctx, params, options) {
3145
3231
  }
3146
3232
  return samlRedirectUrl;
3147
3233
  }
3148
- //#endregion
3149
- //#region src/routes/sso.ts
3234
+ // src/routes/sso.ts
3150
3235
  const BUILT_IN_ACCOUNT_PROVIDER_IDS = [
3151
3236
  "credential",
3152
3237
  "email-otp",
@@ -3944,7 +4029,7 @@ async function handleOIDCCallback(ctx, options, providerId, stateData, parsedPro
3944
4029
  ...Object.fromEntries(Object.entries(mapping.extraFields || {}).map(([key, value]) => [key, rawUserInfo[value]])),
3945
4030
  id: readStringClaim(rawUserInfo, "sub"),
3946
4031
  email: readEmailClaim(rawUserInfo),
3947
- emailVerified: options?.trustEmailVerified ? parseProviderEmailVerified(rawUserInfo[mapping.emailVerified || "email_verified"]) : false,
4032
+ emailVerified: options?.trustEmailVerified ? parseStringBoolean(rawUserInfo[mapping.emailVerified || "email_verified"]) ?? false : false,
3948
4033
  name: readStringClaim(rawUserInfo, mapping.name || "name"),
3949
4034
  image: readStringClaim(rawUserInfo, mapping.image || "picture")
3950
4035
  };
@@ -3955,24 +4040,47 @@ async function handleOIDCCallback(ctx, options, providerId, stateData, parsedPro
3955
4040
  ...Object.fromEntries(Object.entries(mapping.extraFields || {}).map(([key, value]) => [key, idToken[value]])),
3956
4041
  id: idToken.sub,
3957
4042
  email: readEmailClaim(idToken),
3958
- emailVerified: options?.trustEmailVerified ? parseProviderEmailVerified(idToken[mapping.emailVerified || "email_verified"]) : false,
4043
+ emailVerified: options?.trustEmailVerified ? parseStringBoolean(idToken[mapping.emailVerified || "email_verified"]) ?? false : false,
3959
4044
  name: readStringClaim(idToken, mapping.name || "name"),
3960
4045
  image: readStringClaim(idToken, mapping.image || "picture")
3961
4046
  };
3962
4047
  } else return redirectOIDCError("invalid_provider", "user_info_endpoint_not_found");
3963
- if (!userInfo.email || !userInfo.id) return redirectOIDCError("invalid_provider", "missing_user_info");
3964
- const userInfoEmail = userInfo.email;
3965
- const userInfoId = userInfo.id;
4048
+ if (!userInfo.id) return redirectOIDCError("invalid_provider", "missing_user_info");
4049
+ const accountKey = {
4050
+ issuer: verifiedIdToken && readStringClaim(verifiedIdToken.payload, "iss") || provider.issuer,
4051
+ accountId: userInfo.id
4052
+ };
4053
+ const mappedUser = await mapSSOProfile(options, {
4054
+ protocol: "oidc",
4055
+ providerId,
4056
+ accountKey,
4057
+ providerUser: {
4058
+ email: userInfo.email,
4059
+ emailVerified: userInfo.emailVerified === true,
4060
+ name: userInfo.name,
4061
+ image: userInfo.image
4062
+ },
4063
+ providerClaims: rawProfile ?? {},
4064
+ verifiedIdTokenClaims: verifiedIdToken?.payload ?? {}
4065
+ }, ctx.context.logger).catch((e) => {
4066
+ if (isAPIError(e) && e.body?.code) redirectOIDCError(e.body.code, e.body.message);
4067
+ throw e;
4068
+ });
4069
+ if (!mappedUser.email) {
4070
+ ctx.context.logger.error("OIDC provider returned no email. Map an email claim, or create a placeholder with `mapProfileToUser`.", { providerId });
4071
+ return redirectOIDCError("invalid_provider", "missing_user_info");
4072
+ }
4073
+ const userInfoEmail = mappedUser.email;
3966
4074
  const providerUser = {
3967
4075
  ...Object.fromEntries(Object.entries(userInfo).filter(([key]) => key !== "id")),
3968
4076
  email: userInfoEmail,
3969
- name: typeof userInfo.name === "string" ? userInfo.name : "",
3970
- image: typeof userInfo.image === "string" ? userInfo.image : void 0,
3971
- emailVerified: options?.trustEmailVerified ? userInfo.emailVerified === true : false
4077
+ name: mappedUser.name ?? "",
4078
+ image: mappedUser.image,
4079
+ emailVerified: mappedUser.emailVerified
3972
4080
  };
3973
- const accountKey = {
3974
- issuer: verifiedIdToken && readStringClaim(verifiedIdToken.payload, "iss") || provider.issuer,
3975
- accountId: userInfoId
4081
+ const mappedUserInfo = {
4082
+ ...providerUser,
4083
+ id: accountKey.accountId
3976
4084
  };
3977
4085
  const isTrustedProvider = "domainVerified" in provider && provider.domainVerified === true && validateEmailDomain(userInfoEmail, provider.domain);
3978
4086
  let linked;
@@ -4003,19 +4111,12 @@ async function handleOIDCCallback(ctx, options, providerId, stateData, parsedPro
4003
4111
  ...resolution.message === void 0 ? {} : { message: resolution.message }
4004
4112
  });
4005
4113
  const authentication = await handleOAuthUserInfo(ctx, {
4006
- userInfo: {
4007
- ...providerUser,
4008
- email: providerUser.email,
4009
- name: providerUser.name,
4010
- id: userInfoId,
4011
- image: providerUser.image,
4012
- emailVerified: providerUser.emailVerified
4013
- },
4114
+ userInfo: mappedUserInfo,
4014
4115
  account: {
4015
4116
  idToken: tokenResponse.idToken,
4016
4117
  accessToken: tokenResponse.accessToken,
4017
4118
  refreshToken: tokenResponse.refreshToken,
4018
- accountId: userInfoId,
4119
+ accountId: accountKey.accountId,
4019
4120
  providerId: provider.providerId,
4020
4121
  accessTokenExpiresAt: tokenResponse.accessTokenExpiresAt,
4021
4122
  refreshTokenExpiresAt: tokenResponse.refreshTokenExpiresAt,
@@ -4054,7 +4155,7 @@ async function handleOIDCCallback(ctx, options, providerId, stateData, parsedPro
4054
4155
  const { session, user } = linked.data;
4055
4156
  if (options?.provisionUser && (linked.isRegister || options.provisionUserOnEveryLogin)) await options.provisionUser({
4056
4157
  user,
4057
- userInfo,
4158
+ userInfo: mappedUserInfo,
4058
4159
  token: tokenResponse,
4059
4160
  provider
4060
4161
  });
@@ -4063,10 +4164,10 @@ async function handleOIDCCallback(ctx, options, providerId, stateData, parsedPro
4063
4164
  profile: {
4064
4165
  providerType: "oidc",
4065
4166
  providerId: provider.providerId,
4066
- accountId: userInfoId,
4167
+ accountId: accountKey.accountId,
4067
4168
  email: userInfoEmail,
4068
- emailVerified: Boolean(userInfo.emailVerified),
4069
- rawAttributes: userInfo
4169
+ emailVerified: providerUser.emailVerified,
4170
+ rawAttributes: mappedUserInfo
4070
4171
  },
4071
4172
  provider,
4072
4173
  token: tokenResponse,
@@ -4428,7 +4529,7 @@ const initiateSLO = (options) => {
4428
4529
  const session = ctx.context.session;
4429
4530
  const sessionLookupKey = `${SAML_SESSION_BY_ID_PREFIX}${session.session.id}`;
4430
4531
  const sessionLookup = await ctx.context.internalAdapter.findVerificationValue(sessionLookupKey);
4431
- let nameID = session.user.email;
4532
+ let nameID;
4432
4533
  let sessionIndex;
4433
4534
  let samlSessionKey;
4434
4535
  if (sessionLookup) {
@@ -4437,11 +4538,15 @@ const initiateSLO = (options) => {
4437
4538
  if (stored) {
4438
4539
  const data = safeJsonParse(stored.value);
4439
4540
  if (data) {
4440
- nameID = data.nameID || nameID;
4541
+ nameID = data.nameID;
4441
4542
  sessionIndex = data.sessionIndex;
4442
4543
  }
4443
4544
  }
4444
4545
  }
4546
+ if (!nameID) {
4547
+ const [account] = (await ctx.context.internalAdapter.findAccounts(session.user.id)).filter((account) => account.providerId === providerId).sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
4548
+ nameID = account?.accountId ?? session.user.email;
4549
+ }
4445
4550
  const logoutRequest = sp.createLogoutRequest(idp, "redirect", {
4446
4551
  logoutNameID: nameID,
4447
4552
  sessionIndex,
@@ -4460,8 +4565,7 @@ const initiateSLO = (options) => {
4460
4565
  throw ctx.redirect(logoutRequest.context);
4461
4566
  });
4462
4567
  };
4463
- //#endregion
4464
- //#region src/index.ts
4568
+ // src/index.ts
4465
4569
  saml.setSchemaValidator({ async validate(xml) {
4466
4570
  if (XMLValidator.validate(xml, { allowBooleanAttributes: true }) === true) return "SUCCESS_VALIDATE_XML";
4467
4571
  throw "ERR_INVALID_XML";
@@ -4506,6 +4610,53 @@ function assertNoAdditionalFieldCollisions(options) {
4506
4610
  if (builtInFieldNames.has(fieldName)) throw new Error(`ssoProvider additional field "${key}" maps to built-in field "${fieldName}"`);
4507
4611
  }
4508
4612
  }
4613
+ const SAML_SESSION_RECORD_PAGE_SIZE = 100;
4614
+ async function findSAMLSessionRecord(ctx, identifier, isOwnRecord) {
4615
+ for (let offset = 0;; offset += SAML_SESSION_RECORD_PAGE_SIZE) {
4616
+ const page = await ctx.adapter.findMany({
4617
+ model: "verification",
4618
+ where: [{
4619
+ field: "identifier",
4620
+ value: identifier
4621
+ }],
4622
+ sortBy: {
4623
+ field: "createdAt",
4624
+ direction: "desc"
4625
+ },
4626
+ limit: SAML_SESSION_RECORD_PAGE_SIZE,
4627
+ offset
4628
+ });
4629
+ const record = page.find(isOwnRecord);
4630
+ if (record || page.length < SAML_SESSION_RECORD_PAGE_SIZE) return record;
4631
+ }
4632
+ }
4633
+ /**
4634
+ * SLO records are written with the session's expiry at sign-in. Move it when a
4635
+ * refresh extends the session, so expired-row cleanup doesn't drop them early.
4636
+ */
4637
+ async function extendSAMLSessionRecords(ctx, session) {
4638
+ const lookupKey = `${SAML_SESSION_BY_ID_PREFIX}${session.id}`;
4639
+ const lookup = await ctx.internalAdapter.findVerificationValue(lookupKey);
4640
+ const expiresAt = new Date(session.expiresAt);
4641
+ if (!lookup || new Date(lookup.expiresAt) >= expiresAt) return;
4642
+ const isOwnRecord = (record) => safeJsonParse(record.value)?.sessionId === session.id;
4643
+ const latest = await ctx.internalAdapter.findVerificationValue(lookup.value);
4644
+ const secondaryStorageOnly = ctx.options.secondaryStorage && !ctx.options.verification?.storeInDatabase;
4645
+ if (latest && secondaryStorageOnly) {
4646
+ if (isOwnRecord(latest)) await ctx.internalAdapter.updateVerificationByIdentifier(lookup.value, { expiresAt });
4647
+ } else if (latest) {
4648
+ const record = await findSAMLSessionRecord(ctx, latest.identifier, isOwnRecord);
4649
+ if (record) await ctx.adapter.update({
4650
+ model: "verification",
4651
+ where: [{
4652
+ field: "id",
4653
+ value: record.id
4654
+ }],
4655
+ update: { expiresAt }
4656
+ });
4657
+ }
4658
+ await ctx.internalAdapter.updateVerificationByIdentifier(lookupKey, { expiresAt });
4659
+ }
4509
4660
  function sso(options) {
4510
4661
  assertNoAdditionalFieldCollisions(options);
4511
4662
  const optionsWithStore = options;
@@ -4538,8 +4689,13 @@ function sso(options) {
4538
4689
  version: PACKAGE_VERSION,
4539
4690
  init(ctx) {
4540
4691
  const existing = ctx.skipOriginCheck;
4541
- if (existing === true) return {};
4542
- return { context: { skipOriginCheck: [...Array.isArray(existing) ? existing : [], ...SAML_SKIP_ORIGIN_CHECK_PATHS] } };
4692
+ return {
4693
+ ...existing !== true && { context: { skipOriginCheck: [...Array.isArray(existing) ? existing : [], ...SAML_SKIP_ORIGIN_CHECK_PATHS] } },
4694
+ options: { databaseHooks: { session: { update: { async after(session) {
4695
+ if (!options?.saml?.enableSingleLogout) return;
4696
+ await extendSAMLSessionRecords(ctx, session).catch((error) => ctx.logger.warn("Failed to extend SAML session records", { error }));
4697
+ } } } } }
4698
+ };
4543
4699
  },
4544
4700
  endpoints,
4545
4701
  hooks: {
@@ -4629,5 +4785,4 @@ function sso(options) {
4629
4785
  options
4630
4786
  };
4631
4787
  }
4632
- //#endregion
4633
4788
  export { DEFAULT_CLOCK_SKEW_MS, DEFAULT_MAX_SAML_METADATA_SIZE, DEFAULT_MAX_SAML_RESPONSE_SIZE, DataEncryptionAlgorithm, DigestAlgorithm, DiscoveryError, KeyEncryptionAlgorithm, REQUIRED_DISCOVERY_FIELDS, SignatureAlgorithm, computeDiscoveryUrl, deriveSAMLIdentityProviderEntityID, deriveSAMLServiceProviderPolicy, discoverOIDCConfig, fetchDiscoveryDocument, needsRuntimeDiscovery, normalizeDiscoveryUrls, normalizeUrl, selectTokenEndpointAuthMethod, sso, validateDiscoveryDocument, validateDiscoveryUrl, validateSAMLTimestamp };
@@ -0,0 +1,3 @@
1
+ // src/version.ts
2
+ const PACKAGE_VERSION = "1.8.0-beta.9";
3
+ export { PACKAGE_VERSION as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/sso",
3
- "version": "1.8.0-beta.7",
3
+ "version": "1.8.0-beta.9",
4
4
  "description": "SSO plugin for Better Auth",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -64,24 +64,24 @@
64
64
  "zod": "^4.5.4"
65
65
  },
66
66
  "devDependencies": {
67
+ "@better-auth/core": "1.8.0-beta.9",
68
+ "@better-auth/kysely-adapter": "1.8.0-beta.9",
67
69
  "@types/body-parser": "^1.19.6",
68
70
  "@types/express": "^5.0.6",
71
+ "better-auth": "1.8.0-beta.9",
69
72
  "better-call": "1.4.5",
70
73
  "body-parser": "^2.3.0",
71
74
  "express": "^5.2.1",
72
75
  "kysely": "^0.28.17 || ^0.29.0",
73
76
  "oauth2-mock-server": "^9.0.0",
74
- "tsdown": "0.23.1",
75
- "@better-auth/kysely-adapter": "1.8.0-beta.7",
76
- "better-auth": "1.8.0-beta.7",
77
- "@better-auth/core": "1.8.0-beta.7"
77
+ "tsdown": "0.23.1"
78
78
  },
79
79
  "peerDependencies": {
80
+ "@better-auth/core": "^1.8.0-beta.9",
80
81
  "@better-auth/utils": "^0.4.2 || ^0.5.0",
81
82
  "@better-fetch/fetch": "1.3.3",
82
- "better-call": "1.4.5",
83
- "@better-auth/core": "^1.8.0-beta.7",
84
- "better-auth": "^1.8.0-beta.7"
83
+ "better-auth": "^1.8.0-beta.9",
84
+ "better-call": "1.4.5"
85
85
  },
86
86
  "scripts": {
87
87
  "build": "tsdown",
@@ -1,5 +0,0 @@
1
- //#endregion
2
- //#region src/version.ts
3
- const PACKAGE_VERSION = "1.8.0-beta.7";
4
- //#endregion
5
- export { PACKAGE_VERSION as t };