@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
@@ -1,4 +1,4 @@
1
- import { t as PACKAGE_VERSION } from "./version-5EiO_U3Z.mjs";
1
+ import { t as PACKAGE_VERSION } from "./version-DzWb5tB_.mjs";
2
2
  //#region src/client.ts
3
3
  const ssoClient = (options) => {
4
4
  return {
package/dist/index.mjs CHANGED
@@ -1,18 +1,18 @@
1
- import { t as PACKAGE_VERSION } from "./version-5EiO_U3Z.mjs";
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, handleOAuthUserInfo } from "better-auth/oauth2";
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
- if (!needsRuntimeDiscovery(config)) return config;
712
- const hydrated = await discoverOIDCConfig({
713
- issuer,
714
- existingConfig: config,
715
- isTrustedOrigin
716
- });
717
- return {
718
- ...config,
719
- authorizationEndpoint: hydrated.authorizationEndpoint,
720
- tokenEndpoint: hydrated.tokenEndpoint,
721
- tokenEndpointAuthentication: hydrated.tokenEndpointAuthentication,
722
- userInfoEndpoint: hydrated.userInfoEndpoint,
723
- jwksEndpoint: hydrated.jwksEndpoint
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
- const verification = await c.context.internalAdapter.findVerificationValue(`${AUTHN_REQUEST_KEY_PREFIX}${inResponseTo}`);
1130
- if (verification) try {
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, additionalData) {
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 = ctx.context.trustedProviders.includes(providerId) || "domainVerified" in provider && !!provider.domainVerified && validateEmailDomain(userInfo.email, provider.domain);
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 handleOAuthUserInfo(ctx, {
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
- account: {
1973
- providerId,
1974
- accountId: userInfo.id,
1975
- accessToken: "",
1976
- refreshToken: ""
1977
- },
2042
+ providerId,
2043
+ accountId: userInfo.id,
2044
+ tokens: {},
1978
2045
  callbackURL: postAuthRedirect,
1979
2046
  disableSignUp: options?.disableImplicitSignUp,
1980
- isTrustedProvider
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 state = await generateState(ctx, void 0, options?.redirectURI?.trim() ? { ssoProviderId: provider.providerId } : false);
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: ctx.body.scopes || config.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, false);
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, { headers: { Authorization: `Bearer ${tokenResponse.accessToken}` } });
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 handleOAuthUserInfo(ctx, {
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
- account: {
2779
- idToken: tokenResponse.idToken,
2780
- accessToken: tokenResponse.accessToken,
2781
- refreshToken: tokenResponse.refreshToken,
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
- isTrustedProvider
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
- const state = await generateState(ctx, void 0, options?.redirectURI?.trim() ? { ssoProviderId: provider.providerId } : false);
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`);
@@ -1,5 +1,5 @@
1
1
  //#endregion
2
2
  //#region src/version.ts
3
- const PACKAGE_VERSION = "1.7.0-beta.4";
3
+ const PACKAGE_VERSION = "1.7.0-beta.5";
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.7.0-beta.4",
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.5",
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.4",
74
- "better-auth": "1.7.0-beta.4"
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.1.21",
79
- "better-call": "1.3.5",
80
- "@better-auth/core": "^1.7.0-beta.4",
81
- "better-auth": "^1.7.0-beta.4"
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",