Salesforce API
How to get to the “Manage sources” page
Setup with user-to-machine (on-behalf-of) authentication (recommended)
High-level flow
- User logs in with Convier app registration in Entra ID
- Convier requests a token scoped to a Salesforce app registration in Entra on-behalf-of the user
- Convier sends the on-behalf-of token to Salesforce and exchanges it for a Saleforce access token associated with a user in Salesforce
- A custom Apex class in Salesforce validates token signature and audience (that the token belongs to the Salesforce app registration in Entra), and determines user
- The access token is passed along with API requests to salesforce
In Entra
- Create App Registration
Salesforce OIDC- Copy App/client ID (reference:
$entraSalesforceAppId) - Add scope
user_impersonation
- Copy App/client ID (reference:
- Assign Convier App Registration delegated permission
user_impersonationonSalesforce OIDC- Remember to grant consent
In Salesforce
- App:
- Create new External Client App
- External Client App Name:
convier - API Name:
convier - Oauth settings:
- Callback URL:
http://localhost(not in use) - Selected oauth scopes:
Manage user data via APIs (api)
- Callback URL:
- Flow enablement:
- Enable Token Exchange Flow:
checked - Require secret for Token Exchange Flow:
checked
- Enable Token Exchange Flow:
- Under policies, check
Enable Token Exchange Flowunder OAuth Flows and External Client App Enhancements - Copy
Consumer key($consumerKey) andConsumer secret($consumerSecret)
- External Client App Name:
- Create new External Client App
- Security:
- Go to Security -> Remote Site Settings
- Add URL:
https://login.microsoftonline.com
- Token validation:
- Go to Custom Code -> Apex Classes
- Assuming you do not already have an implementation for
Auth.Oauth2TokenExchangeHandler, create one and use the following implementation:
global class ConvierTokenExchangeHandler extends Auth.Oauth2TokenExchangeHandler {
private static final String OIDC_AUDIENCE = '$entraSalesforceAppId';
private static final String EXTERNAL_IDP_JWKS_URL = 'https://login.microsoftonline.com/$tenantId/discovery/v2.0/keys';
private Auth.UserData getUserDataFromJWT(Auth.JWT jwt) {
String email = (String) jwt.getAdditionalClaims().get('upn');
Auth.UserData userData = new Auth.UserData(null, null, null, null, email, null, null, null, null, null, null);
return userData;
}
private User findExistingUser(Auth.UserData data) {
String email = data.email;
List<User> existingUser = [SELECT Id, Username, Email, FirstName, LastName, Alias, ProfileId FROM User WHERE Email=:email LIMIT 1];
if (existingUser.isEmpty()) {
return null;
}
return existingUser[0];
}
// Manual extraction of audience, as built-in method does not seem to exist
private string getAudience(String incomingToken) {
List<String> parts = incomingToken.split('\\.');
Integer pad = Math.mod(4 - Math.mod(parts[1].length(), 4), 4);
String padding = '='.repeat(pad);
String payloadB64 = parts[1].replace('-', '+').replace('_', '/') + padding;
String payloadJson = EncodingUtil.base64Decode(payloadB64).toString();
Map<String, Object> claims = (Map<String, Object>) JSON.deserializeUntyped(payloadJson);
Object audClaim = claims.get('aud');
if (audClaim instanceof String) {
return (String) audClaim;
} else if (audClaim instanceof List<Object>) {
return (String)((List<Object>)audClaim)[0];
}
return null;
}
global override Auth.TokenValidationResult validateIncomingToken(String appDeveloperName, Auth.IntegratingAppType appType, String incomingToken, Auth.OAuth2TokenExchangeType tokenType) {
Auth.JWT jwt = Auth.JWTUtil.validateJWTWithKeysEndpoint(incomingToken, EXTERNAL_IDP_JWKS_URL, true);
String audience = getAudience(incomingToken);
if (!audience.equals(OIDC_AUDIENCE)) {
return new Auth.TokenValidationResult(false);
}
Datetime expClaim = (Datetime) jwt.getAdditionalClaims().get('exp');
if (expClaim < Datetime.now()) {
return new Auth.TokenValidationResult(false);
}
Auth.UserData userData = getUserDataFromJWT(jwt);
if (userData == null) {
return new Auth.TokenValidationResult(false);
}
return new Auth.TokenValidationResult(true, null, userData, incomingToken, tokenType, null);
}
global override Boolean mapToUser(Id networkId, Auth.TokenValidationResult result, Boolean canCreateUser, Boolean canCreateContact, String appDeveloperName, Auth.IntegratingAppType appType) {
return true;
}
global override User getUserForTokenSubject(Id networkId, Auth.TokenValidationResult result, Boolean canCreateUser, String appDeveloperName, Auth.IntegratingAppType appType) {
Auth.UserData data = result.getUserData();
return findExistingUser(data);
}
}
- Go to Identity -> Token Exchange Handlers
- Create a new Token Exchange Handler
- Name:
conviertokenexchangehandler - API Name:
conviertokenexchangehandler - Supported Token Types:
Access Token - Associated Apex Class:
ConvierTokenExchangeHandler - Create
- Name:
- Open token exchange handler details
- Click “Enable New App”
- Click “External Client App”
- Select App
convier - Select run as
your user - Check
Make conviertokenexchangehandler the default handler for this app - Done
In Convier
- Authentication
- Select auth type “On behalf of”
- Select scope
$entraSalesforceAppId/.default - Check “Token exchange”
- URL:
https://$yourDomain.salesforce.com/services/oauth2/token - Client ID:
$consumerKey - Client secret:
$consumerSecret
- URL:
- Config
- Set base URL:
https://$yourDomain.salesforce.com - Add endpoint
whoami- URI:
/services/oauth2/userinfo - Response path:
$ - Click “Get schema”
- Verify that you see your user
- URI:
- Set base URL:
- Save
Setup with mactine-to-machine (client_credentials) authentication
In Salesforce
- Create External Client App, assign appropriate permissions
- Check
Enable Client Credentials Flow - Copy
$consumerKeyand$consumerSecret
In Convier
- Under authentication, select “Username password”
- URL:
https://$yourDomain.salesforce.com/services/oauth2/token - Method:
POST - Add payloads:
- client_id:
$consumerKey - client_secret:
$consumerSecret
- client_id:
- Save