Guide for integrating Convier with the Salesforce API

Contents of this page


Setup with user-to-machine (on-behalf-of) authentication (recommended)

High-level flow

Salesforce auth flow in Convier

Salesforce auth flow in Convier

In Entra

In Salesforce

global class ConvierTokenExchangeHandler extends Auth.Oauth2TokenExchangeHandler {
    public class JwtException extends Exception {}
    private static final String OIDC_ISSUER = '<https://sts.windows.net/$tenantId/>';
    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 AND IsActive=true LIMIT 1];

        if (existingUser.isEmpty()) {
            return null;
        }

        return existingUser[0];
    }

    private string getClaim(String incomingToken, String claim) {
        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(claim);
        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 iss = getClaim(incomingToken, 'iss');

        if (!iss.equals(OIDC_ISSUER)) {
            throw new JwtException('Invalid issuer');
        }

        String audience = getClaim(incomingToken, 'aud');

        if (!audience.equals(OIDC_AUDIENCE)) {
            throw new JwtException('Invalid audience');
        }

        Datetime expClaim = (Datetime) jwt.getAdditionalClaims().get('exp');
          if (expClaim < Datetime.now()) {
              throw new JwtException('Token expired');
          }

        Auth.UserData userData = getUserDataFromJWT(jwt);
        if (userData == null) {
            throw new JwtException('User does not exist or is inactive');
        }

        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);
    }
}

In Convier

Salesforce auth config in Convier

Salesforce auth config in Convier

Setup with mactine-to-machine (client_credentials) authentication

In Salesforce

In Convier

Common errors