To complete the authentication process, you must generate an access token and a Frontdoor URL. Use the generated Frontdoor URL as the authCredential value when you initialize the SDK.
Generate the Access Token
To generate the access token, use the OAuth 2.0 Web Server Flow. The web server flow is a two-step flow for obtaining an access token. Step 1 is browser-driven, where the user redirects to Salesforce and step 2 is server-side, where your backend exchanges the code.
- Authorization request: Your app redirects the user to the Salesforce authorization endpoint,
/services/oauth2/authorize. After the user authenticates, Salesforce redirects back to the configured callback URL with an authorization code in the query string. - Token exchange: Your app sends an HTTP POST request to the Salesforce token endpoint,
/services/oauth2/token, with the authorization code and the app'sclient_id. Salesforce responds with a JSON payload containing theaccess_tokenandinstance_url.
Proof Key for Code Exchange (PKCE) is only required if your External Client App (ECA) has the OAuth security policy Require Proof Key for Code Exchange (PKCE) extension for Supported Authorization Flows enabled. When using this policy, your app must include a code challenge on the authorization request and code verifier on the token request. For more information, see Use the OAuth 2.0 Proof Key for Code Exchange (PKCE) Extension.
If your ECA uses the OAuth security policy Require Secret for Web Server Flow, the POST request to the token endpoint must also include your app's client_secret as a form parameter alongside the client_id. If you're not using this policy, sending the client_id alone is sufficient.
These code examples show detailed steps for authorization. The host references your Salesforce org and CLIENT_ID and CLIENT_SECRET are the values you copied and saved from the ECA OAuth settings. Remember, the consumer key is the client_id and the consumer secret the client_secret.
note: These examles are JavaScript and run on Node.js with Express. You can implement the same flow in any server-side language, substituting the equivalent HTTP and crypto primitives in your stack of choice. For example, use Java with Spring Boot, Python with Flask or FastAPI, Go with net/http, Ruby on Rails, or .NET / C#. The endpoint paths, parameters, and request and response shapes are identical regardless of language.
Shared setup example - the session is required, keeping the token server-side
import express from 'express';
import session from 'express-session';
import crypto from 'node:crypto';
const { LOGIN_URL, CLIENT_ID, CLIENT_SECRET, REDIRECT_URI, APP_ORIGIN, SESSION_SECRET } = process.env;
const app = express();
app.use(session({
secret: SESSION_SECRET,
resave: false,
saveUninitialized: true,
cookie: { httpOnly: true, sameSite: 'lax' },
}));
// --- PKCE helpers ---
const base64url = (buf) =>
buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const generatePkce = () => {
const verifier = base64url(crypto.randomBytes(64));
const challenge = base64url(crypto.createHash('sha256').update(verifier).digest());
return { verifier, challenge };
};
Step 1: Authorization request example
app.get('/login', (req, res) => {
const { verifier, challenge } = generatePkce();
req.session.codeVerifier = verifier;
// Per-session `state` for CSRF / login-mix-up protection. PKCE secures the code
// exchange; `state` is a separate defense on the callback — use both.
const state = base64url(crypto.randomBytes(32));
req.session.oauthState = state;
const params = new URLSearchParams({
response_type: 'code',
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: 'api refresh_token lightning web',
code_challenge: challenge,
code_challenge_method: 'S256',
state,
});
res.redirect(`${LOGIN_URL}/services/oauth2/authorize?${params}`);
});
Step 2: Token exchange example, keeping the token server-side
aapp.get('/getAccessToken', async (req, res) => {
const { code, state, error, error_description } = req.query;
if (error) return res.status(400).send(`OAuth error: ${error_description || error}`);
// Reject the callback unless `state` matches the value stored in Step 1.
if (!state || state !== req.session.oauthState) {
return res.status(400).send('Invalid or missing OAuth state');
}
delete req.session.oauthState; // single-use
const { codeVerifier } = req.session;
if (!code || !codeVerifier) return res.status(400).send('Missing code or PKCE verifier');
const params = new URLSearchParams({
grant_type: 'authorization_code',
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
code: String(code),
code_verifier: String(codeVerifier),
});
if (CLIENT_SECRET) params.append('client_secret', CLIENT_SECRET);
const response = await fetch(`${LOGIN_URL}/services/oauth2/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params.toString(),
});
const data = await response.json();
if (!response.ok || data.error) {
return res
.status(response.status || 500)
.send(`Token exchange failed: ${data.error_description || data.error}`);
}
// Keep credentials server-side. Do NOT send them to the browser.
req.session.accessToken = data.access_token;
req.session.instanceUrl = data.instance_url;
req.session.refreshToken = data.refresh_token; // present if the refresh_token scope was granted
res.redirect(APP_ORIGIN); // send the user back to your app; it then calls /getFrontdoorURL
});
Generate a Frontdoor URL for Embedding
Use Frontdoor URLs to bridge into UI sessions, giving your users uninterrupted access to Salesforce and other apps. The Frontdoor URL uses an existing session to log users into a new UI automatically without making them enter their credentials again. For Tableau Next embedding, only the embedded components need and use the Frontdoor URL.
note: Frontdoor URLs are short-lived. For session refresh, you must generate a new Frontdoor URL.
Generate the Frontdoor URL example
app.get('/getFrontdoorURL', async (req, res) => {
if (!req.session.accessToken || !req.session.instanceUrl) {
return res.status(401).json({ error: 'Not authenticated. Visit /login first.' });
}
const response = await fetch(`${req.session.instanceUrl}/services/oauth2/singleaccess`, {
method: 'POST',
headers: {
accept: 'application/json',
authorization: `Bearer ${req.session.accessToken}`,
'content-type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams(),
});
if (!response.ok) {
return res
.status(response.status)
.json({ error: `Could not generate FrontDoor URL (${response.status}). Re-authenticate at /login.` });
}
const data = await response.json();
// The ONLY value that reaches the browser. Pass it to the SDK as `authCredential`.
res.json({ frontdoorUrl: data.frontdoor_uri });
});
For information on how to use the UI Bridge API to generate Frontdoor URLs, see Generate a Frontdoor URL to Bridge into UI Sessions.
Authentication Management
- Don't hard code OAuth tokens or Frontdoor URLs in your client-side code.
- Pass credentials to the browser only when strictly necessary. Malicious actors can scrape credentials from the browser.
Session Management and Logout
The SDK provides a logout() method to terminate the Salesforce session.
important: Calling logout() tears down the SDK-managed embedded session(s), for the orgs you pass in, or all SDK-initialized orgs if you call it with no arguments. It doesn't sign the user out of unrelated Salesforce sessions in the same browser.
For more information, see logout().