6

I am building out a Salesforce Connect customer adapter to translate rest responses into external data objects.

To authenticate these requests, I would like to use Oauth 2.0's client credentials flow as a custom auth provider.

While developing the AuthProviderPlugin, I am running into two roadblocks, the required initiate and getUserInfo methods.

For the initiate method I am required to return a page reference, but since this is a client credentials flow, I do not need a user to login, I only need to provide the client id, client secret, and access token url.

For the client crendentials flow, what page reference should I return if I do not need a user to login?

I know I need to get the token, but it looks like that should be handled in the handleCallBack method.

For the getUserInfo method, I do not have or need user info, I am simply looking to create an auth provider using Oauth 2.0's client credentials flow to use in my Salesforce Connect Adapter.

Can I return an empty Auth.UserData object back to satisfy the required return?

Lastly...

To me it appears the AuthProviderPluginClass and it's custom adapters do not support Oauth 2.0's client credentials flow by design... is this true? If so, what would be the recommended approach to unattended authentication? Open Id Connect?

Code In Progress:

global class myAuthProvider extends Auth.AuthProviderPluginClass {

  // Use this URL for the endpoint that the 
  // authentication provider calls back to for configuration.
  // *****Where do I need to redirect to?*****
  public String redirectUrl = 'https://REDIRECTWHERE?.COM'; 
  private String clientId;
  private String secret;

  // Application redirection to the website for 
  // authentication and authorization.
  //*****How is this different from the accessTokenUrl?*****
  private String authUrl;  

  // URI to get the new access token
  private String accessTokenUrl; 

  // Api name for the custom metadata type created for this auth provider.
  private String customMetadataTypeApiName = 'myMetaDataApiName'; 

  // Api URL to access the user
  // *****I do need to access the user.*****
  private String userAPIUrl; 

  // Version of the user api URL to access data
  // *****I do not need user data.***** 
  private String userAPIVersionUrl; 

  global String getCustomMetadataType() {
      return customMetadataTypeApiName;
  } 

  global PageReference initiate(Map<string,string> 
    authProviderConfiguration, String stateToPropagate) 
    { 

        clientId = authProviderConfiguration.get('Client_Id__c'); 
        secret = authProviderConfiguration.get('Client_Secret__c'); 
        accessTokenUrl = authProviderConfiguration.get('Access_Token_Url_c');

        String url = accessTokenUrl + '?grant_type=client_credentials&client_id=' + clientId + '&client_secret=' + secret + '&redirect_uri=' + redirectUrl;
        //*****Where do I need to redirect to since this is a client credentials flow, not a user login flow?*****
        return new PageReference(url); 

    } 

    global Auth.AuthProviderTokenResponse handleCallback(Map<string,string> 
    authProviderConfiguration, Auth.AuthProviderCallbackState state ) 
    { 
        // Here, the developer will get the callback with actual protocol. 
        // Their responsibility is to return a new object called 
        // AuthProviderTokenResponse. 
        // This will contain an optional accessToken and refreshToken 
        clientId = authProviderConfiguration.get('Client_Id__c'); 
        secret = authProviderConfiguration.get('Client_Secret__c'); 
        accessTokenUrl = authProviderConfiguration.get('Access_Token__c'); 

        Http http = new Http();
        httpRequest request = new httpRequest();
        request.setHeader('Content-Type', 'application/x-www-form-urlencoded');
        request.setHeader('Content-Length', '0');

        String url = accessTokenUrl;
        request.setEndpoint(url); 

        request.setBody('grant_type=client_credentials&client_id=' + clientId + '&client_secret=' + secret);
        request.setMethod('POST');

        HttpResponse res = new httpResponse();
        system.debug(request);
        res = http.send(request);
        string getTokenString = res.getBody();
        Map<String, Object> results = (Map<String, Object>) JSON.deserializeUntyped(getTokenString);
        string getToken;
        getToken = String.valueOf(results.get('access_token'));

        //*****What if I do not receive/need a refresh token, non-expiring token*****
        return new Auth.AuthProviderTokenResponse('myAuthProvider', getToken, 
        'refreshToken', getToken); 
    } 

    //*****I do not need user data, but a UserData Object must be returned. Can I compile with blank data?*****
    global Auth.UserData getUserInfo(Map<string,string> 
    authProviderConfiguration, 
    Auth.AuthProviderTokenResponse response) 
    { 
        //Here the developer is responsible for constructing an 
        //Auth.UserData object 
        String token = response.oauthToken; 
        HttpRequest req = new HttpRequest(); 
        userAPIUrl = authProviderConfiguration.get('API_User_Url__c');
        userAPIVersionUrl = authProviderConfiguration.get
        ('API_User_Version_Url__c'); 
        req.setHeader('Authorization', 'OAuth ' + token); 
        req.setEndpoint(userAPIUrl); 
        req.setHeader('Content-Type','application/x-www-form-urlencoded'); 
        req.setMethod('GET'); 

        Http http = new Http(); 
        HTTPResponse res = http.send(req); 
        String responseBody = res.getBody(); 
        String id = '';
        String fname = ''; 
        String lname = ''; 
        String flname = ''; 
        String uname = ''; 
        String locale = ''; 
        Map<String,String> provMap = new Map<String,String>(); 
        return new Auth.UserData(id, fname, lname, flname, uname, 'what', locale, null, 'myAuthProvider', null, provMap); 
    } 


}

This related questions appears to support the statement that AuthProviderPluginClass does not support the client credentials flow, but I may be misinterpreting: Custom Auth Provider usage for unattended OAuth flow

3 Answers 3

6

I've written a custom authprovider which I've shared "as-is" on Github. It implements both the Client Credentials and JWT Bearer variant (without a certificate). Feel free to fork from it. https://github.com/bobbywhitesfdc/ApigeeAuthProvider

3
  • 2
    Thanks a lot Bro, using this as a template i was able to write both auth and refresh_token logic
    – Raja Paul
    Dec 16, 2019 at 12:02
  • 1
    Your Apigee custom AuthProvider was very helpful. Thanks.
    – R Roesler
    Jun 7, 2020 at 18:16
  • Do you have a template for custom auth provider method using client assertion JWT instead with ECDSA algorithm?
    – compski
    Apr 2 at 14:24
2

Salesforce Premier Support has stated:

The Custom Auth provider set up in Salesforce would not allow you to enter client credentials and login.

The two posts below also confirm this:

Custom Auth Provider usage for unattended OAuth flow

Named Credentials and support for the OAuth2 Client Credentials Grant Type and alternatives

As a solution to the problem, I added the token callout and pass into the data connection method for the Custom Salesforce Connect adapter. However, I believe it may be better to run a scheduled process every X minutes to update the token in an encrypted custom setting to limit the token callouts.

Referencing DataSourceConnection Class Documentation: https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_class_DataSource_Connection.htm

global class SampleDataSourceConnection extends DataSource.Connection {

        private String EXTERNAL_SRC_URL = 'https://myEndPoint.com/rest-service/comments';
        private final String COL_HEADER_DISPLAYURL = 'DisplayUrl';
        private final String COL_HEADER_COMMENTTITLE = 'CommentTitle';
        private final String COL_HEADER_COMMENTBODY = 'CommentBody';
        private final String COL_HEADER_EXTERNAL_ID = 'ExternalId';
        private string getToken;

        global SampleDataSourceConnection(DataSource.ConnectionParams connectionParams) {
                Http http = new Http();
                httpRequest request = new httpRequest();
                request.setHeader('Content-Type', 'application/x-www-form-urlencoded');
                request.setHeader('Content-Length', '0');
                request.setEndPoint('https://myEndpoint.com/token');
                request.setBody('grant_type=client_credentials&client_id=myClientId&client_secret=myClientSecret);
                request.setMethod('POST'); HttpResponse res = new httpResponse();
                res = http.send(request); 
                string getTokenString = res.getBody();
                Map < String, Object > results = (Map < String, Object > ) JSON.deserializeUntyped(getTokenString); 
                getToken = String.valueOf(results.get('access_token')); system.debug(getToken); 
                System.debug(res.getBody());
                }

                override global List < DataSource.Table > sync() {
                        ....
3
  • 1
    The statement "The Custom Auth provider set up in Salesforce would not allow you to enter client credentials and login." is factually incorrect. See my answer where I link to custom authprovider that does exactly that :-) Jun 9, 2020 at 14:36
  • @BobbyWhite, there seems to be consensus that it is not appropriate to store consumer key and secret in a custom metadata type due to security concerns, but since getCustomMetaDataType is required by the AuthPluginProviderClass that seems to contradict this?
    – S.B.
    Jun 18, 2020 at 17:08
  • 2
    If one is concerned about the safety of the keys, you could encrypt/decrypt it using some secret, but that hasn't been a show stopper so far. Jun 19, 2020 at 18:34
1

Well, if you want to bypass the mandatory PageReference, you could redirect directly to the callback url created in your Auth Provider, example:

global PageReference initiate(Map<string,string> authProviderConfiguration, String stateToPropagate) {
    new PageReference('https://....my.salesforce.com/services/authcallback/YOURAUTHPROVIDER?state=' + stateToPropagate);
}

This way, the Salesforce Admin will never notice he got redirected.

Then, for the getUserInfo, you can bypass it by returning dummy data, such as:

    global Auth.UserData getUserInfo(Map<string,string> authProviderConfiguration, Auth.AuthProviderTokenResponse response) {
        return new Auth.UserData('foo', 'foo', 'foo', 'foo foo', 'uname',
                                 'what', 'en', null, 'YOURAUTHPROVIDER', null, new Map<String,String>());
    }

Then, in the handleCallback method you can make a HTTPRequest to your authentication endpoint using a Named Credential to store the Client Id/Secret in a Named Principal / Username-Password.

I hope this help.

1
  • Going to give this a shot and report back, this looks promising and if it works will mark as the correct answer.
    – S.B.
    May 17, 2019 at 15:21

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .