Salesforce API

How to get to the “Manage sources” page


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
  • Assign Convier App Registration delegated permission user_impersonation on Salesforce 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)
      • Flow enablement:
        • Enable Token Exchange Flow: checked
        • Require secret for Token Exchange Flow: checked
      • Under policies, check Enable Token Exchange Flow under OAuth Flows and External Client App Enhancements
      • Copy Consumer key ($consumerKey) and Consumer secret ($consumerSecret)
  • 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
  • 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
  • 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
  • Save

Setup with mactine-to-machine (client_credentials) authentication

In Salesforce

  • Create External Client App, assign appropriate permissions
  • Check Enable Client Credentials Flow
  • Copy $consumerKey and $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
  • Save