@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
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as PACKAGE_VERSION } from "./version-
|
|
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:
|
|
1797
|
-
email: (
|
|
1798
|
-
name: [
|
|
1799
|
-
emailVerified: options?.trustEmailVerified && mapping.emailVerified ?
|
|
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
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
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
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
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.
|
|
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.
|
|
3028
|
+
if (currentSession?.session) await ctx.context.internalAdapter.deleteSession(currentSession.session.token);
|
|
2991
3029
|
deleteSessionCookie(ctx);
|
|
2992
|
-
const
|
|
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.
|
|
3083
|
+
await ctx.context.internalAdapter.deleteSession(session.session.token);
|
|
3047
3084
|
deleteSessionCookie(ctx);
|
|
3048
3085
|
throw ctx.redirect(logoutRequest.context);
|
|
3049
3086
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/sso",
|
|
3
|
-
"version": "1.6.
|
|
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.
|
|
59
|
+
"fast-xml-parser": "^5.8.0",
|
|
60
60
|
"jose": "^6.1.3",
|
|
61
|
-
"samlify": "
|
|
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.
|
|
74
|
-
"better-auth": "1.6.
|
|
73
|
+
"@better-auth/core": "1.6.13",
|
|
74
|
+
"better-auth": "1.6.13"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
|
-
"@better-auth/utils": "0.4.
|
|
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.
|
|
81
|
-
"better-auth": "^1.6.
|
|
80
|
+
"@better-auth/core": "^1.6.13",
|
|
81
|
+
"better-auth": "^1.6.13"
|
|
82
82
|
},
|
|
83
83
|
"scripts": {
|
|
84
84
|
"build": "tsdown",
|