feat(salesforce): add salesforce provider (#4183)

Co-authored-by: Alex Yang <himself65@outlook.com>
This commit is contained in:
Shobhit Patra
2025-08-26 22:57:31 +05:30
committed by GitHub
parent 557dc39c32
commit da9fcbde85
4 changed files with 329 additions and 0 deletions

View File

@@ -697,6 +697,25 @@ export const contents: Content[] = [
</svg> </svg>
), ),
}, },
{
title: "Salesforce",
href: "/docs/authentication/salesforce",
isNew: true,
icon: () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="1.2em"
height="1.2em"
>
<path
fill="currentColor"
d="M8.5 3.5c-1.4 0-2.5 1.1-2.5 2.5 0 .4.1.7.2 1-1.7.3-3 1.7-3 3.5 0 2 1.6 3.6 3.6 3.6h10.2c1.6 0 2.9-1.3 2.9-2.9 0-1.2-.7-2.2-1.7-2.6 0-.3 0-.5 0-.8-.3-2-1.9-3.5-4-3.3-.4 0-.7.1-1 .2-.5-.8-1.4-1.3-2.4-1.3-.9 0-1.7.4-2.2 1.1zm7.7 7.1c-.5-.3-1.1-.5-1.7-.5-.6 0-1.2.2-1.7.5-.1-2-1.7-3.6-3.8-3.6-1.3 0-2.4.6-3.1 1.6-.4-.2-.8-.3-1.3-.3-1.8 0-3.3 1.5-3.3 3.3 0 .2 0 .5.1.7-1.6.4-2.7 1.8-2.7 3.5 0 2 1.6 3.6 3.6 3.6h10.6c2 0 3.6-1.6 3.6-3.6 0-1.9-1.4-3.4-3.2-3.5z"
/>
</svg>
),
},
{ {
title: "Slack", title: "Slack",
href: "/docs/authentication/slack", href: "/docs/authentication/slack",

View File

@@ -0,0 +1,153 @@
---
title: Salesforce
description: Salesforce provider setup and usage.
---
<Steps>
<Step>
### 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)
<Callout type="info">
- 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
</Callout>
<Callout type="warning">
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.
</Callout>
</Step>
<Step>
### 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]
},
},
})
```
<Callout type="info">
- 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
</Callout>
</Step>
<Step>
### 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
```
</Step>
<Step>
### 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"
})
}
```
</Step>
<Step>
### 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.
<Callout type="info">
The default scopes requested are `openid`, `email`, and `profile`. The provider will automatically include the `id` scope for accessing basic user information.
</Callout>
</Step>
</Steps>

View File

@@ -22,6 +22,7 @@ import { gitlab } from "./gitlab";
import { tiktok } from "./tiktok"; import { tiktok } from "./tiktok";
import { reddit } from "./reddit"; import { reddit } from "./reddit";
import { roblox } from "./roblox"; import { roblox } from "./roblox";
import { salesforce } from "./salesforce";
import { vk } from "./vk"; import { vk } from "./vk";
import { zoom } from "./zoom"; import { zoom } from "./zoom";
import { line } from "./line"; import { line } from "./line";
@@ -48,6 +49,7 @@ export const socialProviders = {
tiktok, tiktok,
reddit, reddit,
roblox, roblox,
salesforce,
vk, vk,
zoom, zoom,
notion, notion,
@@ -91,6 +93,7 @@ export * from "./microsoft-entra-id";
export * from "./notion"; export * from "./notion";
export * from "./reddit"; export * from "./reddit";
export * from "./roblox"; export * from "./roblox";
export * from "./salesforce";
export * from "./spotify"; export * from "./spotify";
export * from "./tiktok"; export * from "./tiktok";
export * from "./twitch"; export * from "./twitch";

View File

@@ -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<SalesforceProfile> {
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<SalesforceProfile>(
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<SalesforceProfile>;
};