From da9fcbde85672f90bef9433e35a3da0dc557aa7c Mon Sep 17 00:00:00 2001 From: Shobhit Patra Date: Tue, 26 Aug 2025 22:57:31 +0530 Subject: [PATCH] feat(salesforce): add salesforce provider (#4183) Co-authored-by: Alex Yang --- docs/components/sidebar-content.tsx | 19 +++ .../docs/authentication/salesforce.mdx | 153 +++++++++++++++++ .../better-auth/src/social-providers/index.ts | 3 + .../src/social-providers/salesforce.ts | 154 ++++++++++++++++++ 4 files changed, 329 insertions(+) create mode 100644 docs/content/docs/authentication/salesforce.mdx create mode 100644 packages/better-auth/src/social-providers/salesforce.ts 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; +};