@better-auth/sso 1.7.0-beta.4 → 1.7.0-beta.5
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,18 +1,18 @@
|
|
|
1
|
-
import { t as PACKAGE_VERSION } from "./version-
|
|
2
|
-
import { APIError, createAuthEndpoint, createAuthMiddleware, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
|
|
1
|
+
import { t as PACKAGE_VERSION } from "./version-DzWb5tB_.mjs";
|
|
2
|
+
import { APIError, addOAuthServerContext, createAuthEndpoint, createAuthMiddleware, getSessionFromCtx, 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";
|
|
6
6
|
import { generateRandomString } from "better-auth/crypto";
|
|
7
7
|
import * as z from "zod";
|
|
8
|
-
import { isPublicRoutableHost } from "@better-auth/core/utils/host";
|
|
8
|
+
import { classifyHost, 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
11
|
import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
|
|
12
12
|
import { isAPIError } from "@better-auth/core/utils/is-api-error";
|
|
13
13
|
import { HIDE_METADATA, PRIVATE_KEY_JWT_SIGNING_ALGORITHMS, createAuthorizationURL, createPrivateKeyJwtClientAssertionGetter, generateGenericState, generateState, parseGenericState, parseState, validateAuthorizationCode, validateToken } from "better-auth";
|
|
14
14
|
import { deleteSessionCookie, setSessionCookie } from "better-auth/cookies";
|
|
15
|
-
import { additionalAuthorizationParamsSchema,
|
|
15
|
+
import { additionalAuthorizationParamsSchema, signInWithOAuthIdentity } from "better-auth/oauth2";
|
|
16
16
|
import { decodeJwt } from "jose";
|
|
17
17
|
import * as samlifyNamespace from "samlify";
|
|
18
18
|
import samlifyDefault from "samlify";
|
|
@@ -527,6 +527,75 @@ function validateSkipDiscoveryEndpoints(config, isTrustedOrigin) {
|
|
|
527
527
|
for (const [name, url] of fields) if (url) validateSkipDiscoveryEndpoint(name, url, isTrustedOrigin);
|
|
528
528
|
}
|
|
529
529
|
/**
|
|
530
|
+
* Re-validate an endpoint by resolving its hostname and rejecting any resolved
|
|
531
|
+
* address that is not publicly routable.
|
|
532
|
+
*
|
|
533
|
+
* {@link validateSkipDiscoveryEndpoint} only classifies the literal hostname, so
|
|
534
|
+
* a host like `idp.example` whose DNS record points at `127.0.0.1`,
|
|
535
|
+
* `169.254.169.254`, or an RFC 1918 address passes that check unchanged. This
|
|
536
|
+
* function closes that gap by performing the same RFC 6890 classification on the
|
|
537
|
+
* addresses the host actually resolves to, right before the server-side fetch.
|
|
538
|
+
*
|
|
539
|
+
* Best-effort by design:
|
|
540
|
+
* - Operator-allowlisted origins (trustedOrigins) are skipped — this is the
|
|
541
|
+
* documented escape hatch for internal IdPs.
|
|
542
|
+
* - IP-literal hosts are already fully covered by the synchronous check.
|
|
543
|
+
* - On runtimes without `node:dns` (e.g. Cloudflare Workers / edge), DNS
|
|
544
|
+
* resolution is unavailable; we fall back to the synchronous host check and
|
|
545
|
+
* the platform's own egress controls.
|
|
546
|
+
*
|
|
547
|
+
* Note: this resolves once and validates the result; it does not pin the address
|
|
548
|
+
* for the subsequent connection, so a change in the resolved address between
|
|
549
|
+
* this lookup and the fetch remains theoretically possible. It nonetheless
|
|
550
|
+
* rejects the common case of a DNS record that statically points at an internal
|
|
551
|
+
* address.
|
|
552
|
+
*
|
|
553
|
+
* @throws DiscoveryError(discovery_private_host) if any resolved address is not public
|
|
554
|
+
*/
|
|
555
|
+
async function assertEndpointResolvesPublic(name, endpoint, isTrustedOrigin) {
|
|
556
|
+
const parsed = parseURL(name, endpoint);
|
|
557
|
+
if (isTrustedOrigin(parsed.toString())) return;
|
|
558
|
+
const host = parsed.hostname;
|
|
559
|
+
if (classifyHost(host).literal !== "fqdn") return;
|
|
560
|
+
let dns;
|
|
561
|
+
try {
|
|
562
|
+
dns = await import("node:dns/promises");
|
|
563
|
+
} catch {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
let resolved;
|
|
567
|
+
try {
|
|
568
|
+
resolved = await dns.lookup(host, { all: true });
|
|
569
|
+
} catch {
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
for (const { address } of resolved) if (!isPublicRoutableHost(address)) throw new DiscoveryError("discovery_private_host", `The ${name} host "${host}" resolves to a non-publicly-routable address (${address}). If this is an internal IdP, add its origin to trustedOrigins.`, {
|
|
573
|
+
endpoint: name,
|
|
574
|
+
url: endpoint,
|
|
575
|
+
hostname: host,
|
|
576
|
+
resolved: address
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Re-validate, at fetch time, every OIDC endpoint that is fetched server-side
|
|
581
|
+
* (token, userinfo, jwks). Runs the synchronous host classification plus the
|
|
582
|
+
* best-effort DNS resolution check. `authorizationEndpoint` is intentionally
|
|
583
|
+
* excluded — it is a browser redirect target, not a server-side fetch, so these
|
|
584
|
+
* checks don't apply to it.
|
|
585
|
+
*/
|
|
586
|
+
async function assertOIDCEndpointsResolvePublic(config, isTrustedOrigin) {
|
|
587
|
+
const fields = [
|
|
588
|
+
["tokenEndpoint", config.tokenEndpoint],
|
|
589
|
+
["userInfoEndpoint", config.userInfoEndpoint],
|
|
590
|
+
["jwksEndpoint", config.jwksEndpoint]
|
|
591
|
+
];
|
|
592
|
+
for (const [name, url] of fields) {
|
|
593
|
+
if (!url) continue;
|
|
594
|
+
validateSkipDiscoveryEndpoint(name, url, isTrustedOrigin);
|
|
595
|
+
await assertEndpointResolvesPublic(name, url, isTrustedOrigin);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
530
599
|
* Fetch the OIDC discovery document from the IdP.
|
|
531
600
|
*
|
|
532
601
|
* @param url - The discovery endpoint URL
|
|
@@ -538,7 +607,8 @@ async function fetchDiscoveryDocument(url, timeout = DEFAULT_DISCOVERY_TIMEOUT)
|
|
|
538
607
|
try {
|
|
539
608
|
const response = await betterFetch(url, {
|
|
540
609
|
method: "GET",
|
|
541
|
-
timeout
|
|
610
|
+
timeout,
|
|
611
|
+
redirect: "error"
|
|
542
612
|
});
|
|
543
613
|
if (response.error) {
|
|
544
614
|
const { status } = response.error;
|
|
@@ -708,20 +778,24 @@ function needsRuntimeDiscovery(config) {
|
|
|
708
778
|
* Throws if discovery fails.
|
|
709
779
|
*/
|
|
710
780
|
async function ensureRuntimeDiscovery(config, issuer, isTrustedOrigin) {
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
781
|
+
let resolved = config;
|
|
782
|
+
if (needsRuntimeDiscovery(config)) {
|
|
783
|
+
const hydrated = await discoverOIDCConfig({
|
|
784
|
+
issuer,
|
|
785
|
+
existingConfig: config,
|
|
786
|
+
isTrustedOrigin
|
|
787
|
+
});
|
|
788
|
+
resolved = {
|
|
789
|
+
...config,
|
|
790
|
+
authorizationEndpoint: hydrated.authorizationEndpoint,
|
|
791
|
+
tokenEndpoint: hydrated.tokenEndpoint,
|
|
792
|
+
tokenEndpointAuthentication: hydrated.tokenEndpointAuthentication,
|
|
793
|
+
userInfoEndpoint: hydrated.userInfoEndpoint,
|
|
794
|
+
jwksEndpoint: hydrated.jwksEndpoint
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
await assertOIDCEndpointsResolvePublic(resolved, isTrustedOrigin);
|
|
798
|
+
return resolved;
|
|
725
799
|
}
|
|
726
800
|
//#endregion
|
|
727
801
|
//#region src/oidc/errors.ts
|
|
@@ -1125,11 +1199,10 @@ async function validateInResponseTo(c, ctx) {
|
|
|
1125
1199
|
const inResponseTo = ctx.extract.response?.inResponseTo;
|
|
1126
1200
|
const allowIdpInitiated = ctx.options.allowIdpInitiated ?? false;
|
|
1127
1201
|
if (inResponseTo) {
|
|
1202
|
+
const consumed = await c.context.internalAdapter.consumeVerificationValue(`${AUTHN_REQUEST_KEY_PREFIX}${inResponseTo}`);
|
|
1128
1203
|
let storedRequest = null;
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
storedRequest = JSON.parse(verification.value);
|
|
1132
|
-
if (storedRequest && storedRequest.expiresAt < Date.now()) storedRequest = null;
|
|
1204
|
+
if (consumed) try {
|
|
1205
|
+
storedRequest = JSON.parse(consumed.value);
|
|
1133
1206
|
} catch {
|
|
1134
1207
|
storedRequest = null;
|
|
1135
1208
|
}
|
|
@@ -1146,10 +1219,8 @@ async function validateInResponseTo(c, ctx) {
|
|
|
1146
1219
|
expectedProvider: storedRequest.providerId,
|
|
1147
1220
|
actualProvider: ctx.providerId
|
|
1148
1221
|
});
|
|
1149
|
-
await c.context.internalAdapter.deleteVerificationByIdentifier(`${AUTHN_REQUEST_KEY_PREFIX}${inResponseTo}`);
|
|
1150
1222
|
throw c.redirect(errorRedirectUrl(ctx.redirectUrl, "invalid_saml_response", "Provider mismatch"));
|
|
1151
1223
|
}
|
|
1152
|
-
await c.context.internalAdapter.deleteVerificationByIdentifier(`${AUTHN_REQUEST_KEY_PREFIX}${inResponseTo}`);
|
|
1153
1224
|
} else if (!allowIdpInitiated) {
|
|
1154
1225
|
c.context.logger.error("SAML IdP-initiated SSO rejected: InResponseTo missing and allowIdpInitiated is false", { providerId: ctx.providerId });
|
|
1155
1226
|
throw c.redirect(errorRedirectUrl(ctx.redirectUrl, "unsolicited_response", "IdP-initiated SSO not allowed"));
|
|
@@ -1590,14 +1661,12 @@ const deleteSSOProvider = () => {
|
|
|
1590
1661
|
};
|
|
1591
1662
|
//#endregion
|
|
1592
1663
|
//#region src/saml-state.ts
|
|
1593
|
-
async function generateRelayState(c, link
|
|
1664
|
+
async function generateRelayState(c, link) {
|
|
1594
1665
|
const callbackURL = c.body.callbackURL;
|
|
1595
1666
|
if (!callbackURL) throw new APIError("BAD_REQUEST", { message: "callbackURL is required" });
|
|
1596
|
-
const codeVerifier = generateRandomString(128);
|
|
1597
1667
|
const stateData = {
|
|
1598
|
-
...additionalData ? additionalData : {},
|
|
1599
1668
|
callbackURL,
|
|
1600
|
-
codeVerifier,
|
|
1669
|
+
codeVerifier: generateRandomString(128),
|
|
1601
1670
|
errorURL: c.body.errorCallbackURL,
|
|
1602
1671
|
newUserURL: c.body.newUserCallbackURL,
|
|
1603
1672
|
link,
|
|
@@ -1699,7 +1768,8 @@ function createSP(config, baseURL, providerId, opts) {
|
|
|
1699
1768
|
isAssertionEncrypted: spData?.isAssertionEncrypted || false,
|
|
1700
1769
|
encPrivateKey: normalizePem(spData?.encPrivateKey),
|
|
1701
1770
|
encPrivateKeyPass: spData?.encPrivateKeyPass,
|
|
1702
|
-
relayState: opts?.relayState
|
|
1771
|
+
relayState: opts?.relayState,
|
|
1772
|
+
clockDrifts: opts?.clockSkew && opts?.clockSkew !== 0 ? [-opts.clockSkew, opts.clockSkew] : void 0
|
|
1703
1773
|
});
|
|
1704
1774
|
}
|
|
1705
1775
|
function createIdP(config) {
|
|
@@ -1855,7 +1925,7 @@ async function processSAMLResponse(ctx, params, options) {
|
|
|
1855
1925
|
if (options?.domainVerification?.enabled && !("domainVerified" in provider && provider.domainVerified)) throw new APIError("UNAUTHORIZED", { message: "Provider domain has not been verified" });
|
|
1856
1926
|
const parsedSamlConfig = typeof provider.samlConfig === "object" ? provider.samlConfig : safeJsonParse(provider.samlConfig);
|
|
1857
1927
|
if (!parsedSamlConfig) throw new APIError("BAD_REQUEST", { message: "Invalid SAML configuration" });
|
|
1858
|
-
const sp = createSP(parsedSamlConfig, ctx.context.baseURL, providerId);
|
|
1928
|
+
const sp = createSP(parsedSamlConfig, ctx.context.baseURL, providerId, { clockSkew: options?.saml?.clockSkew });
|
|
1859
1929
|
const idp = createIdP(parsedSamlConfig);
|
|
1860
1930
|
const samlRedirectUrl = getSafeRedirectUrl(relayState?.callbackURL, params.currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
|
|
1861
1931
|
validateSingleAssertion(SAMLResponse);
|
|
@@ -1957,27 +2027,32 @@ async function processSAMLResponse(ctx, params, options) {
|
|
|
1957
2027
|
});
|
|
1958
2028
|
throw new APIError("BAD_REQUEST", { message: "Unable to extract user ID or email from SAML response" });
|
|
1959
2029
|
}
|
|
1960
|
-
const isTrustedProvider =
|
|
2030
|
+
const isTrustedProvider = "domainVerified" in provider && !!provider.domainVerified && validateEmailDomain(userInfo.email, provider.domain);
|
|
1961
2031
|
const postAuthRedirect = relayState?.callbackURL || ctx.context.baseURL;
|
|
1962
2032
|
const errorUrl = relayState?.errorURL || samlRedirectUrl;
|
|
1963
2033
|
let result;
|
|
1964
2034
|
try {
|
|
1965
|
-
result = await
|
|
2035
|
+
result = await signInWithOAuthIdentity(ctx, {
|
|
1966
2036
|
userInfo: {
|
|
1967
2037
|
email: userInfo.email,
|
|
1968
2038
|
name: userInfo.name || userInfo.email,
|
|
1969
2039
|
id: userInfo.id,
|
|
1970
2040
|
emailVerified: Boolean(userInfo.emailVerified)
|
|
1971
2041
|
},
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
accessToken: "",
|
|
1976
|
-
refreshToken: ""
|
|
1977
|
-
},
|
|
2042
|
+
providerId,
|
|
2043
|
+
accountId: userInfo.id,
|
|
2044
|
+
tokens: {},
|
|
1978
2045
|
callbackURL: postAuthRedirect,
|
|
1979
2046
|
disableSignUp: options?.disableImplicitSignUp,
|
|
1980
|
-
|
|
2047
|
+
source: {
|
|
2048
|
+
method: "sso-saml",
|
|
2049
|
+
sso: {
|
|
2050
|
+
providerId,
|
|
2051
|
+
profile: attributes
|
|
2052
|
+
}
|
|
2053
|
+
},
|
|
2054
|
+
isTrustedProvider,
|
|
2055
|
+
trustProviderByName: false
|
|
1981
2056
|
});
|
|
1982
2057
|
} catch (e) {
|
|
1983
2058
|
if (isAPIError(e) && e.body?.code) {
|
|
@@ -2274,6 +2349,14 @@ const registerSSOProvider = (options) => {
|
|
|
2274
2349
|
if (!member) throw new APIError("BAD_REQUEST", { message: "You are not a member of the organization" });
|
|
2275
2350
|
if (ctx.context.hasPlugin("organization") && !hasOrgAdminRole(member)) throw new APIError("FORBIDDEN", { message: "You must be an organization owner or admin to register SSO providers" });
|
|
2276
2351
|
}
|
|
2352
|
+
if (new Set([
|
|
2353
|
+
"credential",
|
|
2354
|
+
...ctx.context.socialProviders.map((p) => p.id),
|
|
2355
|
+
...ctx.context.trustedProviders
|
|
2356
|
+
]).has(body.providerId)) {
|
|
2357
|
+
ctx.context.logger.warn(`SSO provider registration rejected for reserved providerId: ${body.providerId}`);
|
|
2358
|
+
throw new APIError("UNPROCESSABLE_ENTITY", { message: "This providerId is reserved and cannot be used for an SSO provider" });
|
|
2359
|
+
}
|
|
2277
2360
|
if (await ctx.context.adapter.findOne({
|
|
2278
2361
|
model: "ssoProvider",
|
|
2279
2362
|
where: [{
|
|
@@ -2589,9 +2672,16 @@ const signInSSO = (options) => {
|
|
|
2589
2672
|
throw error;
|
|
2590
2673
|
}
|
|
2591
2674
|
if (!config.authorizationEndpoint) throw new APIError("BAD_REQUEST", { message: "Invalid OIDC configuration. Authorization URL not found." });
|
|
2592
|
-
const
|
|
2675
|
+
const requestedScopes = ctx.body.scopes || config.scopes || [
|
|
2676
|
+
"openid",
|
|
2677
|
+
"email",
|
|
2678
|
+
"profile",
|
|
2679
|
+
"offline_access"
|
|
2680
|
+
];
|
|
2681
|
+
if (options?.redirectURI?.trim()) await addOAuthServerContext({ ssoProviderId: provider.providerId });
|
|
2682
|
+
const state = await generateState(ctx, { requestedScopes });
|
|
2593
2683
|
const redirectURI = getOIDCRedirectURI(ctx.context.baseURL, provider.providerId, options);
|
|
2594
|
-
const authorizationURL = await createAuthorizationURL({
|
|
2684
|
+
const { url: authorizationURL } = await createAuthorizationURL({
|
|
2595
2685
|
id: provider.issuer,
|
|
2596
2686
|
options: {
|
|
2597
2687
|
clientId: config.clientId,
|
|
@@ -2600,12 +2690,7 @@ const signInSSO = (options) => {
|
|
|
2600
2690
|
redirectURI,
|
|
2601
2691
|
state: state.state,
|
|
2602
2692
|
codeVerifier: config.pkce ? state.codeVerifier : void 0,
|
|
2603
|
-
scopes:
|
|
2604
|
-
"openid",
|
|
2605
|
-
"email",
|
|
2606
|
-
"profile",
|
|
2607
|
-
"offline_access"
|
|
2608
|
-
],
|
|
2693
|
+
scopes: requestedScopes,
|
|
2609
2694
|
loginHint: ctx.body.loginHint || email,
|
|
2610
2695
|
authorizationEndpoint: config.authorizationEndpoint,
|
|
2611
2696
|
additionalParams: ctx.body.additionalParams
|
|
@@ -2620,7 +2705,7 @@ const signInSSO = (options) => {
|
|
|
2620
2705
|
const parsedSamlConfig = typeof provider.samlConfig === "object" ? provider.samlConfig : safeJsonParse(provider.samlConfig);
|
|
2621
2706
|
if (!parsedSamlConfig) throw new APIError("BAD_REQUEST", { message: "Invalid SAML configuration" });
|
|
2622
2707
|
if (parsedSamlConfig.authnRequestsSigned && !parsedSamlConfig.spMetadata?.privateKey && !parsedSamlConfig.privateKey) throw new APIError("BAD_REQUEST", { message: "authnRequestsSigned is enabled but no privateKey provided in spMetadata or samlConfig" });
|
|
2623
|
-
const { state: relayState } = await generateRelayState(ctx, void 0
|
|
2708
|
+
const { state: relayState } = await generateRelayState(ctx, void 0);
|
|
2624
2709
|
const sp = createSP(parsedSamlConfig, ctx.context.baseURL, provider.providerId, { relayState });
|
|
2625
2710
|
const idp = createIdP(parsedSamlConfig);
|
|
2626
2711
|
const loginRequest = sp.createLoginRequest(idp, "redirect");
|
|
@@ -2668,7 +2753,7 @@ async function handleOIDCCallback(ctx, options, providerId, stateData) {
|
|
|
2668
2753
|
const errorURL = ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`;
|
|
2669
2754
|
throw ctx.redirect(`${errorURL}?error=invalid_state`);
|
|
2670
2755
|
}
|
|
2671
|
-
const { callbackURL, errorURL, newUserURL, requestSignUp } = stateData;
|
|
2756
|
+
const { callbackURL, errorURL, newUserURL, requestSignUp, requestedScopes } = stateData;
|
|
2672
2757
|
if (!code || error) throw ctx.redirect(`${errorURL || callbackURL}?error=${error}&error_description=${error_description}`);
|
|
2673
2758
|
const provider = await resolveOIDCProvider(ctx, options, providerId);
|
|
2674
2759
|
if (!provider) throw ctx.redirect(`${errorURL || callbackURL}?error=invalid_provider&error_description=provider not found`);
|
|
@@ -2731,10 +2816,15 @@ async function handleOIDCCallback(ctx, options, providerId, stateData) {
|
|
|
2731
2816
|
if (!tokenResponse) throw ctx.redirect(`${errorURL || callbackURL}?error=invalid_provider&error_description=token_response_not_found`);
|
|
2732
2817
|
let userInfo = null;
|
|
2733
2818
|
const mapping = config.mapping || {};
|
|
2819
|
+
let rawProfile;
|
|
2734
2820
|
if (config.userInfoEndpoint) {
|
|
2735
|
-
const userInfoResponse = await betterFetch(config.userInfoEndpoint, {
|
|
2821
|
+
const userInfoResponse = await betterFetch(config.userInfoEndpoint, {
|
|
2822
|
+
headers: { Authorization: `Bearer ${tokenResponse.accessToken}` },
|
|
2823
|
+
redirect: "error"
|
|
2824
|
+
});
|
|
2736
2825
|
if (userInfoResponse.error) throw ctx.redirect(`${errorURL || callbackURL}?error=invalid_provider&error_description=${userInfoResponse.error.message}`);
|
|
2737
2826
|
const rawUserInfo = userInfoResponse.data;
|
|
2827
|
+
rawProfile = rawUserInfo;
|
|
2738
2828
|
userInfo = {
|
|
2739
2829
|
...Object.fromEntries(Object.entries(mapping.extraFields || {}).map(([key, value]) => [key, rawUserInfo[value]])),
|
|
2740
2830
|
id: rawUserInfo[mapping.id || "sub"],
|
|
@@ -2745,6 +2835,7 @@ async function handleOIDCCallback(ctx, options, providerId, stateData) {
|
|
|
2745
2835
|
};
|
|
2746
2836
|
} else if (tokenResponse.idToken) {
|
|
2747
2837
|
const idToken = decodeJwt(tokenResponse.idToken);
|
|
2838
|
+
rawProfile = idToken;
|
|
2748
2839
|
if (!config.jwksEndpoint) throw ctx.redirect(`${errorURL || callbackURL}?error=invalid_provider&error_description=jwks_endpoint_not_found`);
|
|
2749
2840
|
const verified = await validateToken(tokenResponse.idToken, config.jwksEndpoint, {
|
|
2750
2841
|
audience: config.clientId,
|
|
@@ -2767,7 +2858,7 @@ async function handleOIDCCallback(ctx, options, providerId, stateData) {
|
|
|
2767
2858
|
const isTrustedProvider = "domainVerified" in provider && provider.domainVerified === true && validateEmailDomain(userInfo.email, provider.domain);
|
|
2768
2859
|
let linked;
|
|
2769
2860
|
try {
|
|
2770
|
-
linked = await
|
|
2861
|
+
linked = await signInWithOAuthIdentity(ctx, {
|
|
2771
2862
|
userInfo: {
|
|
2772
2863
|
email: userInfo.email,
|
|
2773
2864
|
name: userInfo.name || "",
|
|
@@ -2775,20 +2866,22 @@ async function handleOIDCCallback(ctx, options, providerId, stateData) {
|
|
|
2775
2866
|
image: userInfo.image,
|
|
2776
2867
|
emailVerified: options?.trustEmailVerified ? userInfo.emailVerified || false : false
|
|
2777
2868
|
},
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
accountId: userInfo.id,
|
|
2783
|
-
providerId: provider.providerId,
|
|
2784
|
-
accessTokenExpiresAt: tokenResponse.accessTokenExpiresAt,
|
|
2785
|
-
refreshTokenExpiresAt: tokenResponse.refreshTokenExpiresAt,
|
|
2786
|
-
scope: tokenResponse.scopes?.join(",")
|
|
2787
|
-
},
|
|
2869
|
+
providerId: provider.providerId,
|
|
2870
|
+
accountId: userInfo.id,
|
|
2871
|
+
tokens: tokenResponse,
|
|
2872
|
+
requestedScopes,
|
|
2788
2873
|
callbackURL,
|
|
2789
2874
|
disableSignUp: options?.disableImplicitSignUp && !requestSignUp,
|
|
2790
2875
|
overrideUserInfo: config.overrideUserInfo,
|
|
2791
|
-
|
|
2876
|
+
source: {
|
|
2877
|
+
method: "sso-oidc",
|
|
2878
|
+
sso: {
|
|
2879
|
+
providerId: provider.providerId,
|
|
2880
|
+
profile: rawProfile
|
|
2881
|
+
}
|
|
2882
|
+
},
|
|
2883
|
+
isTrustedProvider,
|
|
2884
|
+
trustProviderByName: false
|
|
2792
2885
|
});
|
|
2793
2886
|
} catch (e) {
|
|
2794
2887
|
if (isAPIError(e) && e.body?.code) {
|
|
@@ -2906,9 +2999,15 @@ async function bounceIfIdpInitiated(ctx, options, providerId) {
|
|
|
2906
2999
|
});
|
|
2907
3000
|
return;
|
|
2908
3001
|
}
|
|
2909
|
-
|
|
3002
|
+
if (options?.redirectURI?.trim()) await addOAuthServerContext({ ssoProviderId: provider.providerId });
|
|
3003
|
+
const state = await generateState(ctx, { requestedScopes: config.scopes || [
|
|
3004
|
+
"openid",
|
|
3005
|
+
"email",
|
|
3006
|
+
"profile",
|
|
3007
|
+
"offline_access"
|
|
3008
|
+
] });
|
|
2910
3009
|
const redirectURI = getOIDCRedirectURI(ctx.context.baseURL, provider.providerId, options);
|
|
2911
|
-
const authorizationURL = await createAuthorizationURL({
|
|
3010
|
+
const { url: authorizationURL } = await createAuthorizationURL({
|
|
2912
3011
|
id: provider.issuer,
|
|
2913
3012
|
options: {
|
|
2914
3013
|
clientId: config.clientId,
|
|
@@ -2957,7 +3056,7 @@ const callbackSSOShared = (options) => {
|
|
|
2957
3056
|
const errorURL = ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`;
|
|
2958
3057
|
throw ctx.redirect(`${errorURL}?error=invalid_state`);
|
|
2959
3058
|
}
|
|
2960
|
-
const providerId = stateData.ssoProviderId;
|
|
3059
|
+
const providerId = stateData.serverContext?.ssoProviderId;
|
|
2961
3060
|
if (!providerId) {
|
|
2962
3061
|
const errorURL = stateData.errorURL || stateData.callbackURL;
|
|
2963
3062
|
throw ctx.redirect(`${errorURL}?error=invalid_state&error_description=missing_provider_id`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/sso",
|
|
3
|
-
"version": "1.7.0-beta.
|
|
3
|
+
"version": "1.7.0-beta.5",
|
|
4
4
|
"description": "SSO plugin for Better Auth",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -65,20 +65,20 @@
|
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@types/body-parser": "^1.19.6",
|
|
67
67
|
"@types/express": "^5.0.6",
|
|
68
|
-
"better-call": "1.3.
|
|
68
|
+
"better-call": "1.3.6",
|
|
69
69
|
"body-parser": "^2.2.2",
|
|
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.7.0-beta.
|
|
74
|
-
"better-auth": "1.7.0-beta.
|
|
73
|
+
"@better-auth/core": "1.7.0-beta.5",
|
|
74
|
+
"better-auth": "1.7.0-beta.5"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
77
|
"@better-auth/utils": "0.4.1",
|
|
78
|
-
"@better-fetch/fetch": "1.
|
|
79
|
-
"better-call": "1.3.
|
|
80
|
-
"@better-auth/core": "^1.7.0-beta.
|
|
81
|
-
"better-auth": "^1.7.0-beta.
|
|
78
|
+
"@better-fetch/fetch": "1.2.2",
|
|
79
|
+
"better-call": "1.3.6",
|
|
80
|
+
"@better-auth/core": "^1.7.0-beta.5",
|
|
81
|
+
"better-auth": "^1.7.0-beta.5"
|
|
82
82
|
},
|
|
83
83
|
"scripts": {
|
|
84
84
|
"build": "tsdown",
|