@better-auth/sso 1.6.11 → 1.6.13

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.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { t as PACKAGE_VERSION } from "./version-D_ggtAOl.mjs";
1
+ import { t as PACKAGE_VERSION } from "./version-BcVD6vO4.mjs";
2
2
  //#region src/client.ts
3
3
  const ssoClient = (options) => {
4
4
  return {
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { t as PACKAGE_VERSION } from "./version-D_ggtAOl.mjs";
1
+ import { t as PACKAGE_VERSION } from "./version-BcVD6vO4.mjs";
2
2
  import { APIError, createAuthEndpoint, createAuthMiddleware, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
3
3
  import { XMLParser, XMLValidator } from "fast-xml-parser";
4
4
  import { X509Certificate } from "node:crypto";
@@ -8,6 +8,7 @@ import * as z from "zod";
8
8
  import { isPublicRoutableHost } from "@better-auth/core/utils/host";
9
9
  import { BetterFetchError, betterFetch } from "@better-fetch/fetch";
10
10
  import { base64 } from "@better-auth/utils/base64";
11
+ import { isAPIError } from "@better-auth/core/utils/is-api-error";
11
12
  import { HIDE_METADATA, createAuthorizationURL, generateGenericState, generateState, parseGenericState, parseState, validateAuthorizationCode, validateToken } from "better-auth";
12
13
  import { deleteSessionCookie, setSessionCookie } from "better-auth/cookies";
13
14
  import { handleOAuthUserInfo } from "better-auth/oauth2";
@@ -56,7 +57,6 @@ const DEFAULT_MAX_SAML_RESPONSE_SIZE = 256 * 1024;
56
57
  * Protects against oversized metadata documents.
57
58
  */
58
59
  const DEFAULT_MAX_SAML_METADATA_SIZE = 100 * 1024;
59
- const SAML_STATUS_SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success";
60
60
  //#endregion
61
61
  //#region src/utils.ts
62
62
  /**
@@ -103,6 +103,10 @@ function parseCertificate(certPem) {
103
103
  publicKeyAlgorithm: cert.publicKey.asymmetricKeyType?.toUpperCase() || "UNKNOWN"
104
104
  };
105
105
  }
106
+ function normalizePem(key) {
107
+ if (!key) return key;
108
+ return `${key.split("\n").map((line) => line.trim()).join("\n").trim()}\n`;
109
+ }
106
110
  function getHostnameFromDomain(domain) {
107
111
  return getHostname(domain) || null;
108
112
  }
@@ -1528,10 +1532,10 @@ function createSP(config, baseURL, providerId, opts) {
1528
1532
  wantLogoutRequestSigned: opts?.sloOptions?.wantLogoutRequestSigned ?? false,
1529
1533
  wantLogoutResponseSigned: opts?.sloOptions?.wantLogoutResponseSigned ?? false,
1530
1534
  metadata: spData?.metadata,
1531
- privateKey: spData?.privateKey || config.privateKey,
1535
+ privateKey: normalizePem(spData?.privateKey || config.privateKey),
1532
1536
  privateKeyPass: spData?.privateKeyPass,
1533
1537
  isAssertionEncrypted: spData?.isAssertionEncrypted || false,
1534
- encPrivateKey: spData?.encPrivateKey,
1538
+ encPrivateKey: normalizePem(spData?.encPrivateKey),
1535
1539
  encPrivateKeyPass: spData?.encPrivateKeyPass,
1536
1540
  nameIDFormat: config.identifierFormat ? [config.identifierFormat] : void 0,
1537
1541
  relayState: opts?.relayState
@@ -1541,10 +1545,10 @@ function createIdP(config) {
1541
1545
  const idpData = config.idpMetadata;
1542
1546
  if (idpData?.metadata) return saml.IdentityProvider({
1543
1547
  metadata: idpData.metadata,
1544
- privateKey: idpData.privateKey,
1548
+ privateKey: normalizePem(idpData.privateKey),
1545
1549
  privateKeyPass: idpData.privateKeyPass,
1546
1550
  isAssertionEncrypted: idpData.isAssertionEncrypted,
1547
- encPrivateKey: idpData.encPrivateKey,
1551
+ encPrivateKey: normalizePem(idpData.encPrivateKey),
1548
1552
  encPrivateKeyPass: idpData.encPrivateKeyPass
1549
1553
  });
1550
1554
  return saml.IdentityProvider({
@@ -1557,7 +1561,7 @@ function createIdP(config) {
1557
1561
  signingCert: idpData?.cert || config.cert,
1558
1562
  wantAuthnRequestsSigned: config.authnRequestsSigned || false,
1559
1563
  isAssertionEncrypted: idpData?.isAssertionEncrypted || false,
1560
- encPrivateKey: idpData?.encPrivateKey,
1564
+ encPrivateKey: normalizePem(idpData?.encPrivateKey),
1561
1565
  encPrivateKeyPass: idpData?.encPrivateKeyPass
1562
1566
  });
1563
1567
  }
@@ -1791,12 +1795,16 @@ async function processSAMLResponse(ctx, params, options) {
1791
1795
  } else ctx.context.logger.warn("Could not extract assertion ID for replay protection", { providerId });
1792
1796
  const attributes = extract.attributes || {};
1793
1797
  const mapping = parsedSamlConfig.mapping ?? {};
1798
+ const attr = (key) => {
1799
+ const value = attributes[key];
1800
+ return Array.isArray(value) ? value[0] : value;
1801
+ };
1794
1802
  const userInfo = {
1795
1803
  ...Object.fromEntries(Object.entries(mapping.extraFields || {}).map(([key, value]) => [key, attributes[value]])),
1796
- id: attributes[mapping.id || "nameID"] || extract.nameID,
1797
- email: (attributes[mapping.email || "email"] || extract.nameID).toLowerCase(),
1798
- name: [attributes[mapping.firstName || "givenName"], attributes[mapping.lastName || "surname"]].filter(Boolean).join(" ") || attributes[mapping.name || "displayName"] || extract.nameID,
1799
- emailVerified: options?.trustEmailVerified && mapping.emailVerified ? attributes[mapping.emailVerified] || false : false
1804
+ id: attr(mapping.id || "nameID") || extract.nameID,
1805
+ email: (attr(mapping.email || "email") || extract.nameID || "").toLowerCase(),
1806
+ name: [attr(mapping.firstName || "givenName"), attr(mapping.lastName || "surname")].filter(Boolean).join(" ") || attr(mapping.name || "displayName") || extract.nameID,
1807
+ emailVerified: options?.trustEmailVerified && mapping.emailVerified ? attr(mapping.emailVerified) || false : false
1800
1808
  };
1801
1809
  if (!userInfo.id || !userInfo.email) {
1802
1810
  ctx.context.logger.error("Missing essential user info from SAML response", {
@@ -1809,23 +1817,35 @@ async function processSAMLResponse(ctx, params, options) {
1809
1817
  }
1810
1818
  const isTrustedProvider = ctx.context.trustedProviders.includes(providerId) || "domainVerified" in provider && !!provider.domainVerified && validateEmailDomain(userInfo.email, provider.domain);
1811
1819
  const callbackUrl = relayState?.callbackURL || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
1812
- const result = await handleOAuthUserInfo(ctx, {
1813
- userInfo: {
1814
- email: userInfo.email,
1815
- name: userInfo.name || userInfo.email,
1816
- id: userInfo.id,
1817
- emailVerified: Boolean(userInfo.emailVerified)
1818
- },
1819
- account: {
1820
- providerId,
1821
- accountId: userInfo.id,
1822
- accessToken: "",
1823
- refreshToken: ""
1824
- },
1825
- callbackURL: callbackUrl,
1826
- disableSignUp: options?.disableImplicitSignUp,
1827
- isTrustedProvider
1828
- });
1820
+ const errorUrl = relayState?.errorURL || samlRedirectUrl;
1821
+ let result;
1822
+ try {
1823
+ result = await handleOAuthUserInfo(ctx, {
1824
+ userInfo: {
1825
+ email: userInfo.email,
1826
+ name: userInfo.name || userInfo.email,
1827
+ id: userInfo.id,
1828
+ emailVerified: Boolean(userInfo.emailVerified)
1829
+ },
1830
+ account: {
1831
+ providerId,
1832
+ accountId: userInfo.id,
1833
+ accessToken: "",
1834
+ refreshToken: ""
1835
+ },
1836
+ callbackURL: callbackUrl,
1837
+ disableSignUp: options?.disableImplicitSignUp,
1838
+ isTrustedProvider
1839
+ });
1840
+ } catch (e) {
1841
+ if (isAPIError(e) && e.body?.code) {
1842
+ const params = new URLSearchParams({ error: e.body.code });
1843
+ if (e.body.message) params.set("error_description", e.body.message);
1844
+ const sep = errorUrl.includes("?") ? "&" : "?";
1845
+ throw ctx.redirect(`${errorUrl}${sep}${params.toString()}`);
1846
+ }
1847
+ throw e;
1848
+ }
1829
1849
  if (result.error) throw ctx.redirect(`${callbackUrl}?error=${result.error.split(" ").join("_")}`);
1830
1850
  const { session, user } = result.data;
1831
1851
  if (options?.provisionUser && (result.isRegister || options.provisionUserOnEveryLogin)) await options.provisionUser({
@@ -1854,6 +1874,7 @@ async function processSAMLResponse(ctx, params, options) {
1854
1874
  const samlSessionKey = `${SAML_SESSION_KEY_PREFIX}${providerId}:${extract.nameID}`;
1855
1875
  const samlSessionData = {
1856
1876
  sessionId: session.id,
1877
+ sessionToken: session.token,
1857
1878
  providerId,
1858
1879
  nameID: extract.nameID,
1859
1880
  sessionIndex: extract.sessionIndex
@@ -2530,7 +2551,7 @@ const signInSSO = (options) => {
2530
2551
  const sp = saml.ServiceProvider({
2531
2552
  metadata,
2532
2553
  allowCreate: true,
2533
- privateKey: parsedSamlConfig.spMetadata?.privateKey || parsedSamlConfig.privateKey,
2554
+ privateKey: normalizePem(parsedSamlConfig.spMetadata?.privateKey || parsedSamlConfig.privateKey),
2534
2555
  privateKeyPass: parsedSamlConfig.spMetadata?.privateKeyPass,
2535
2556
  relayState
2536
2557
  });
@@ -2545,15 +2566,15 @@ const signInSSO = (options) => {
2545
2566
  signingCert: idpData?.cert || parsedSamlConfig.cert,
2546
2567
  wantAuthnRequestsSigned: parsedSamlConfig.authnRequestsSigned || false,
2547
2568
  isAssertionEncrypted: idpData?.isAssertionEncrypted || false,
2548
- encPrivateKey: idpData?.encPrivateKey,
2569
+ encPrivateKey: normalizePem(idpData?.encPrivateKey),
2549
2570
  encPrivateKeyPass: idpData?.encPrivateKeyPass
2550
2571
  });
2551
2572
  else idp = saml.IdentityProvider({
2552
2573
  metadata: idpData.metadata,
2553
- privateKey: idpData.privateKey,
2574
+ privateKey: normalizePem(idpData.privateKey),
2554
2575
  privateKeyPass: idpData.privateKeyPass,
2555
2576
  isAssertionEncrypted: idpData.isAssertionEncrypted,
2556
- encPrivateKey: idpData.encPrivateKey,
2577
+ encPrivateKey: normalizePem(idpData.encPrivateKey),
2557
2578
  encPrivateKeyPass: idpData.encPrivateKeyPass
2558
2579
  });
2559
2580
  const loginRequest = sp.createLoginRequest(idp, "redirect");
@@ -2698,30 +2719,47 @@ async function handleOIDCCallback(ctx, options, providerId, stateData) {
2698
2719
  } else throw ctx.redirect(`${errorURL || callbackURL}?error=invalid_provider&error_description=user_info_endpoint_not_found`);
2699
2720
  if (!userInfo.email || !userInfo.id) throw ctx.redirect(`${errorURL || callbackURL}?error=invalid_provider&error_description=missing_user_info`);
2700
2721
  const isTrustedProvider = "domainVerified" in provider && provider.domainVerified === true && validateEmailDomain(userInfo.email, provider.domain);
2701
- const linked = await handleOAuthUserInfo(ctx, {
2702
- userInfo: {
2703
- email: userInfo.email,
2704
- name: userInfo.name || "",
2705
- id: userInfo.id,
2706
- image: userInfo.image,
2707
- emailVerified: options?.trustEmailVerified ? userInfo.emailVerified || false : false
2708
- },
2709
- account: {
2710
- idToken: tokenResponse.idToken,
2711
- accessToken: tokenResponse.accessToken,
2712
- refreshToken: tokenResponse.refreshToken,
2713
- accountId: userInfo.id,
2714
- providerId: provider.providerId,
2715
- accessTokenExpiresAt: tokenResponse.accessTokenExpiresAt,
2716
- refreshTokenExpiresAt: tokenResponse.refreshTokenExpiresAt,
2717
- scope: tokenResponse.scopes?.join(",")
2718
- },
2719
- callbackURL,
2720
- disableSignUp: options?.disableImplicitSignUp && !requestSignUp,
2721
- overrideUserInfo: config.overrideUserInfo,
2722
- isTrustedProvider
2723
- });
2724
- if (linked.error) throw ctx.redirect(`${errorURL || callbackURL}?error=${linked.error}`);
2722
+ let linked;
2723
+ try {
2724
+ linked = await handleOAuthUserInfo(ctx, {
2725
+ userInfo: {
2726
+ email: userInfo.email,
2727
+ name: userInfo.name || "",
2728
+ id: userInfo.id,
2729
+ image: userInfo.image,
2730
+ emailVerified: options?.trustEmailVerified ? userInfo.emailVerified || false : false
2731
+ },
2732
+ account: {
2733
+ idToken: tokenResponse.idToken,
2734
+ accessToken: tokenResponse.accessToken,
2735
+ refreshToken: tokenResponse.refreshToken,
2736
+ accountId: userInfo.id,
2737
+ providerId: provider.providerId,
2738
+ accessTokenExpiresAt: tokenResponse.accessTokenExpiresAt,
2739
+ refreshTokenExpiresAt: tokenResponse.refreshTokenExpiresAt,
2740
+ scope: tokenResponse.scopes?.join(",")
2741
+ },
2742
+ callbackURL,
2743
+ disableSignUp: options?.disableImplicitSignUp && !requestSignUp,
2744
+ overrideUserInfo: config.overrideUserInfo,
2745
+ isTrustedProvider
2746
+ });
2747
+ } catch (e) {
2748
+ if (isAPIError(e) && e.body?.code) {
2749
+ const baseURL = errorURL || callbackURL;
2750
+ const params = new URLSearchParams({ error: e.body.code });
2751
+ if (e.body.message) params.set("error_description", e.body.message);
2752
+ const sep = baseURL.includes("?") ? "&" : "?";
2753
+ throw ctx.redirect(`${baseURL}${sep}${params.toString()}`);
2754
+ }
2755
+ throw e;
2756
+ }
2757
+ if (linked.error) {
2758
+ const baseURL = errorURL || callbackURL;
2759
+ const params = new URLSearchParams({ error: linked.error });
2760
+ const sep = baseURL.includes("?") ? "&" : "?";
2761
+ throw ctx.redirect(`${baseURL}${sep}${params.toString()}`);
2762
+ }
2725
2763
  const { session, user } = linked.data;
2726
2764
  if (options?.provisionUser && (linked.isRegister || options.provisionUserOnEveryLogin)) await options.provisionUser({
2727
2765
  user,
@@ -2977,7 +3015,7 @@ async function handleLogoutRequest(ctx, sp, idp, relayState, providerId) {
2977
3015
  if (stored) {
2978
3016
  const data = safeJsonParse(stored.value);
2979
3017
  if (data) if (!sessionIndex || !data.sessionIndex || sessionIndex === data.sessionIndex) {
2980
- await ctx.context.internalAdapter.deleteSession(data.sessionId).catch((e) => ctx.context.logger.warn("Failed to delete session during SLO", { error: e }));
3018
+ await ctx.context.internalAdapter.deleteSession(data.sessionToken).catch((e) => ctx.context.logger.warn("Failed to delete session during SLO", { error: e }));
2981
3019
  await ctx.context.internalAdapter.deleteVerificationByIdentifier(`${SAML_SESSION_BY_ID_PREFIX}${data.sessionId}`).catch((e) => ctx.context.logger.warn("Failed to delete SAML session lookup during SLO", e));
2982
3020
  } else ctx.context.logger.warn("SessionIndex mismatch in LogoutRequest - skipping session deletion", {
2983
3021
  providerId,
@@ -2987,10 +3025,9 @@ async function handleLogoutRequest(ctx, sp, idp, relayState, providerId) {
2987
3025
  await ctx.context.internalAdapter.deleteVerificationByIdentifier(key).catch((e) => ctx.context.logger.warn("Failed to delete SAML session key during SLO", e));
2988
3026
  }
2989
3027
  const currentSession = await getSessionFromCtx(ctx);
2990
- if (currentSession?.session) await ctx.context.internalAdapter.deleteSession(currentSession.session.id);
3028
+ if (currentSession?.session) await ctx.context.internalAdapter.deleteSession(currentSession.session.token);
2991
3029
  deleteSessionCookie(ctx);
2992
- const requestId = parsed.extract.request?.id || "";
2993
- const res = sp.createLogoutResponse(idp, null, binding, relayState || "", (template) => template.replace("{InResponseTo}", requestId).replace("{StatusCode}", SAML_STATUS_SUCCESS));
3030
+ const res = sp.createLogoutResponse(idp, parsed, binding, relayState || "");
2994
3031
  if (binding === "post" && res.entityEndpoint) return createSAMLPostForm(res.entityEndpoint, "SAMLResponse", res.context, relayState);
2995
3032
  throw ctx.redirect(res.context);
2996
3033
  }
@@ -3043,7 +3080,7 @@ const initiateSLO = (options) => {
3043
3080
  });
3044
3081
  if (samlSessionKey) await ctx.context.internalAdapter.deleteVerificationByIdentifier(samlSessionKey).catch((e) => ctx.context.logger.warn("Failed to delete SAML session key during logout", e));
3045
3082
  await ctx.context.internalAdapter.deleteVerificationByIdentifier(sessionLookupKey).catch((e) => ctx.context.logger.warn("Failed to delete session lookup key during logout", e));
3046
- await ctx.context.internalAdapter.deleteSession(session.session.id);
3083
+ await ctx.context.internalAdapter.deleteSession(session.session.token);
3047
3084
  deleteSessionCookie(ctx);
3048
3085
  throw ctx.redirect(logoutRequest.context);
3049
3086
  });
@@ -1,5 +1,5 @@
1
1
  //#endregion
2
2
  //#region src/version.ts
3
- const PACKAGE_VERSION = "1.6.11";
3
+ const PACKAGE_VERSION = "1.6.13";
4
4
  //#endregion
5
5
  export { PACKAGE_VERSION as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/sso",
3
- "version": "1.6.11",
3
+ "version": "1.6.13",
4
4
  "description": "SSO plugin for Better Auth",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -56,9 +56,9 @@
56
56
  }
57
57
  },
58
58
  "dependencies": {
59
- "fast-xml-parser": "^5.5.7",
59
+ "fast-xml-parser": "^5.8.0",
60
60
  "jose": "^6.1.3",
61
- "samlify": "~2.10.2",
61
+ "samlify": "^2.13.1",
62
62
  "tldts": "^6.1.0",
63
63
  "zod": "^4.3.6"
64
64
  },
@@ -70,15 +70,15 @@
70
70
  "express": "^5.2.1",
71
71
  "oauth2-mock-server": "^8.2.2",
72
72
  "tsdown": "0.21.1",
73
- "@better-auth/core": "1.6.11",
74
- "better-auth": "1.6.11"
73
+ "@better-auth/core": "1.6.13",
74
+ "better-auth": "1.6.13"
75
75
  },
76
76
  "peerDependencies": {
77
- "@better-auth/utils": "0.4.0",
77
+ "@better-auth/utils": "0.4.1",
78
78
  "@better-fetch/fetch": "1.1.21",
79
79
  "better-call": "1.3.5",
80
- "@better-auth/core": "^1.6.11",
81
- "better-auth": "^1.6.11"
80
+ "@better-auth/core": "^1.6.13",
81
+ "better-auth": "^1.6.13"
82
82
  },
83
83
  "scripts": {
84
84
  "build": "tsdown",