mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-07 12:27:44 +00:00
feat(salesforce): add salesforce provider (#4183)
Co-authored-by: Alex Yang <himself65@outlook.com>
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
153
docs/content/docs/authentication/salesforce.mdx
Normal file
153
docs/content/docs/authentication/salesforce.mdx
Normal 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>
|
||||||
@@ -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";
|
||||||
|
|||||||
154
packages/better-auth/src/social-providers/salesforce.ts
Normal file
154
packages/better-auth/src/social-providers/salesforce.ts
Normal 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>;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user