diff --git a/docs/components/sidebar-content.tsx b/docs/components/sidebar-content.tsx
index b26b3baf..0b8dd102 100644
--- a/docs/components/sidebar-content.tsx
+++ b/docs/components/sidebar-content.tsx
@@ -697,6 +697,25 @@ export const contents: Content[] = [
),
},
+ {
+ title: "Salesforce",
+ href: "/docs/authentication/salesforce",
+ isNew: true,
+ icon: () => (
+
+ ),
+ },
+
{
title: "Slack",
href: "/docs/authentication/slack",
diff --git a/docs/content/docs/authentication/salesforce.mdx b/docs/content/docs/authentication/salesforce.mdx
new file mode 100644
index 00000000..1f8dc386
--- /dev/null
+++ b/docs/content/docs/authentication/salesforce.mdx
@@ -0,0 +1,153 @@
+---
+title: Salesforce
+description: Salesforce provider setup and usage.
+---
+
+
+
+ ### Get your Salesforce Credentials
+ 1. Log into your Salesforce org (Production or Developer Edition)
+ 2. Navigate to **Setup > App Manager**
+ 3. Click **New Connected App**
+ 4. Fill in the basic information:
+ - Connected App Name: Your app name
+ - API Name: Auto-generated from app name
+ - Contact Email: Your email address
+ 5. Enable OAuth Settings:
+ - Check **Enable OAuth Settings**
+ - Set **Callback URL** to your redirect URI (e.g., `http://localhost:3000/api/auth/callback/salesforce` for development)
+ - Select Required OAuth Scopes:
+ - Access your basic information (id)
+ - Access your identity URL service (openid)
+ - Access your email address (email)
+ - Perform requests on your behalf at any time (refresh_token, offline_access)
+ 6. Enable **Require Proof Key for Code Exchange (PKCE)** (required)
+ 7. Save and note your **Consumer Key** (Client ID) and **Consumer Secret** (Client Secret)
+
+
+ - For development, you can use `http://localhost:3000` URLs, but production requires HTTPS
+ - The callback URL must exactly match what's configured in Better Auth
+ - PKCE (Proof Key for Code Exchange) is required by Salesforce and is automatically handled by the provider
+
+
+
+ For sandbox testing, you can create the Connected App in your sandbox org, or use the same Connected App but specify `environment: "sandbox"` in the provider configuration.
+
+
+
+
+
+ ### Configure the provider
+ To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
+
+ ```ts title="auth.ts"
+ import { betterAuth } from "better-auth"
+
+ export const auth = betterAuth({
+ socialProviders: {
+ salesforce: { // [!code highlight]
+ clientId: process.env.SALESFORCE_CLIENT_ID as string, // [!code highlight]
+ clientSecret: process.env.SALESFORCE_CLIENT_SECRET as string, // [!code highlight]
+ environment: "production", // or "sandbox" // [!code highlight]
+ }, // [!code highlight]
+ },
+ })
+ ```
+
+ #### Configuration Options
+
+ - `clientId`: Your Connected App's Consumer Key
+ - `clientSecret`: Your Connected App's Consumer Secret
+ - `environment`: `"production"` (default) or `"sandbox"`
+ - `loginUrl`: Custom My Domain URL (without `https://`) - overrides environment setting
+ - `redirectURI`: Override the auto-generated redirect URI if needed
+
+ #### Advanced Configuration
+
+ ```ts title="auth.ts"
+ export const auth = betterAuth({
+ socialProviders: {
+ salesforce: {
+ clientId: process.env.SALESFORCE_CLIENT_ID as string,
+ clientSecret: process.env.SALESFORCE_CLIENT_SECRET as string,
+ environment: "sandbox", // [!code highlight]
+ loginUrl: "mycompany.my.salesforce.com", // Custom My Domain // [!code highlight]
+ redirectURI: "http://localhost:3000/api/auth/callback/salesforce", // Override if needed // [!code highlight]
+ },
+ },
+ })
+ ```
+
+
+ - Use `environment: "sandbox"` for testing with Salesforce sandbox orgs
+ - The `loginUrl` option is useful for organizations with My Domain enabled
+ - The `redirectURI` option helps resolve redirect URI mismatch errors
+
+
+
+
+ ### Environment Variables
+ Add the following environment variables to your `.env.local` file:
+
+ ```bash title=".env.local"
+ SALESFORCE_CLIENT_ID=your_consumer_key_here
+ SALESFORCE_CLIENT_SECRET=your_consumer_secret_here
+ BETTER_AUTH_URL=http://localhost:3000 # Important for redirect URI generation
+ ```
+
+ For production:
+ ```bash title=".env"
+ SALESFORCE_CLIENT_ID=your_consumer_key_here
+ SALESFORCE_CLIENT_SECRET=your_consumer_secret_here
+ BETTER_AUTH_URL=https://yourdomain.com
+ ```
+
+
+
+ ### Sign In with Salesforce
+ To sign in with Salesforce, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
+ - `provider`: The provider to use. It should be set to `salesforce`.
+
+ ```ts title="auth-client.ts"
+ import { createAuthClient } from "better-auth/client"
+ const authClient = createAuthClient()
+
+ const signIn = async () => {
+ const data = await authClient.signIn.social({
+ provider: "salesforce"
+ })
+ }
+ ```
+
+
+ ### Troubleshooting
+
+ #### Redirect URI Mismatch Error
+ If you encounter a `redirect_uri_mismatch` error:
+
+ 1. **Check Callback URL**: Ensure the Callback URL in your Salesforce Connected App exactly matches your Better Auth callback URL
+ 2. **Protocol**: Make sure you're using the same protocol (`http://` vs `https://`)
+ 3. **Port**: Verify the port number matches (e.g., `:3000`)
+ 4. **Override if needed**: Use the `redirectURI` option to explicitly set the redirect URI
+
+ ```ts
+ salesforce: {
+ clientId: process.env.SALESFORCE_CLIENT_ID as string,
+ clientSecret: process.env.SALESFORCE_CLIENT_SECRET as string,
+ redirectURI: "http://localhost:3000/api/auth/callback/salesforce", // [!code highlight]
+ }
+ ```
+
+ #### Environment Issues
+ - **Production**: Use `environment: "production"` (default) with `login.salesforce.com`
+ - **Sandbox**: Use `environment: "sandbox"` with `test.salesforce.com`
+ - **My Domain**: Use `loginUrl: "yourcompany.my.salesforce.com"` for custom domains
+
+ #### PKCE Requirements
+ Salesforce requires PKCE (Proof Key for Code Exchange) which is automatically handled by this provider. Make sure PKCE is enabled in your Connected App settings.
+
+
+ The default scopes requested are `openid`, `email`, and `profile`. The provider will automatically include the `id` scope for accessing basic user information.
+
+
+
diff --git a/packages/better-auth/src/social-providers/index.ts b/packages/better-auth/src/social-providers/index.ts
index 0588261e..13cafba6 100644
--- a/packages/better-auth/src/social-providers/index.ts
+++ b/packages/better-auth/src/social-providers/index.ts
@@ -22,6 +22,7 @@ import { gitlab } from "./gitlab";
import { tiktok } from "./tiktok";
import { reddit } from "./reddit";
import { roblox } from "./roblox";
+import { salesforce } from "./salesforce";
import { vk } from "./vk";
import { zoom } from "./zoom";
import { line } from "./line";
@@ -48,6 +49,7 @@ export const socialProviders = {
tiktok,
reddit,
roblox,
+ salesforce,
vk,
zoom,
notion,
@@ -91,6 +93,7 @@ export * from "./microsoft-entra-id";
export * from "./notion";
export * from "./reddit";
export * from "./roblox";
+export * from "./salesforce";
export * from "./spotify";
export * from "./tiktok";
export * from "./twitch";
diff --git a/packages/better-auth/src/social-providers/salesforce.ts b/packages/better-auth/src/social-providers/salesforce.ts
new file mode 100644
index 00000000..8db4c35d
--- /dev/null
+++ b/packages/better-auth/src/social-providers/salesforce.ts
@@ -0,0 +1,154 @@
+import { betterFetch } from "@better-fetch/fetch";
+import { decodeJwt } from "jose";
+import { BetterAuthError } from "../error";
+import type { OAuthProvider, ProviderOptions } from "../oauth2";
+import { createAuthorizationURL, validateAuthorizationCode } from "../oauth2";
+import { logger } from "../utils/logger";
+import { refreshAccessToken } from "../oauth2/refresh-access-token";
+
+export interface SalesforceProfile {
+ sub: string;
+ user_id: string;
+ organization_id: string;
+ preferred_username?: string;
+ email: string;
+ email_verified?: boolean;
+ name: string;
+ given_name?: string;
+ family_name?: string;
+ zoneinfo?: string;
+ photos?: {
+ picture?: string;
+ thumbnail?: string;
+ };
+}
+
+export interface SalesforceOptions extends ProviderOptions {
+ environment?: "sandbox" | "production";
+ loginUrl?: string;
+ /**
+ * Override the redirect URI if auto-detection fails.
+ * Should match the Callback URL configured in your Salesforce Connected App.
+ * @example "http://localhost:3000/api/auth/callback/salesforce"
+ */
+ redirectURI?: string;
+}
+
+export const salesforce = (options: SalesforceOptions) => {
+ const environment = options.environment ?? "production";
+ const isSandbox = environment === "sandbox";
+ const authorizationEndpoint = options.loginUrl
+ ? `https://${options.loginUrl}/services/oauth2/authorize`
+ : isSandbox
+ ? "https://test.salesforce.com/services/oauth2/authorize"
+ : "https://login.salesforce.com/services/oauth2/authorize";
+
+ const tokenEndpoint = options.loginUrl
+ ? `https://${options.loginUrl}/services/oauth2/token`
+ : isSandbox
+ ? "https://test.salesforce.com/services/oauth2/token"
+ : "https://login.salesforce.com/services/oauth2/token";
+
+ const userInfoEndpoint = options.loginUrl
+ ? `https://${options.loginUrl}/services/oauth2/userinfo`
+ : isSandbox
+ ? "https://test.salesforce.com/services/oauth2/userinfo"
+ : "https://login.salesforce.com/services/oauth2/userinfo";
+
+ return {
+ id: "salesforce",
+ name: "Salesforce",
+
+ async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
+ if (!options.clientId || !options.clientSecret) {
+ logger.error(
+ "Client Id and Client Secret are required for Salesforce. Make sure to provide them in the options.",
+ );
+ throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
+ }
+ if (!codeVerifier) {
+ throw new BetterAuthError("codeVerifier is required for Salesforce");
+ }
+
+ const _scopes = options.disableDefaultScope
+ ? []
+ : ["openid", "email", "profile"];
+ options.scope && _scopes.push(...options.scope);
+ scopes && _scopes.push(...scopes);
+
+ return createAuthorizationURL({
+ id: "salesforce",
+ options,
+ authorizationEndpoint,
+ scopes: _scopes,
+ state,
+ codeVerifier,
+ redirectURI: options.redirectURI || redirectURI,
+ });
+ },
+
+ validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
+ return validateAuthorizationCode({
+ code,
+ codeVerifier,
+ redirectURI: options.redirectURI || redirectURI,
+ options,
+ tokenEndpoint,
+ });
+ },
+
+ refreshAccessToken: options.refreshAccessToken
+ ? options.refreshAccessToken
+ : async (refreshToken) => {
+ return refreshAccessToken({
+ refreshToken,
+ options: {
+ clientId: options.clientId,
+ clientSecret: options.clientSecret,
+ },
+ tokenEndpoint,
+ });
+ },
+
+ async getUserInfo(token) {
+ if (options.getUserInfo) {
+ return options.getUserInfo(token);
+ }
+
+ try {
+ const { data: user } = await betterFetch(
+ userInfoEndpoint,
+ {
+ headers: {
+ Authorization: `Bearer ${token.accessToken}`,
+ },
+ },
+ );
+
+ if (!user) {
+ logger.error("Failed to fetch user info from Salesforce");
+ return null;
+ }
+
+ const userMap = await options.mapProfileToUser?.(user);
+
+ return {
+ user: {
+ id: user.user_id,
+ name: user.name,
+ email: user.email,
+ image: user.photos?.picture || user.photos?.thumbnail,
+ emailVerified: user.email_verified ?? false,
+ ...userMap,
+ },
+ data: user,
+ };
+ } catch (error) {
+ logger.error("Failed to fetch user info from Salesforce:", error);
+ return null;
+ }
+ },
+
+ options,
+ } satisfies OAuthProvider;
+};