[SalesForce] Custom OAuth Auth Provider

I have a Rest API Endpoint that I am looking to callout to.

Something like this: https://myrestservice.com/service/logs

When setting up the Named Credential,

I set up the URL to: https://myrestservice.com/service/logs

I select OAuth 2.0 as my provider and I am then prompted to select an authentication provider.

I have not created one and when I go to create one and I am provided a list of pre-defined provider types, none of which are my providers.

The documentation indicates I need to create a custom metadata type: https://help.salesforce.com/articleView?id=sso_provider_plugin_custom.htm&type=5#plugin_build

I create the MetaData Type myProvider and add two custom fields to store the Client Id, Client Secret, and Access Token.

Documentation then indicates I need to create a class that extends Auth.AuthProviderPluginClass. Documentation here: https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_class_Auth_AuthProviderPluginClass.htm#apex_class_Auth_AuthProviderPluginClass

In my custom provider, I generate an access token, and I am provided with the following:

  • Access Token URL
  • Client ID
  • Client Secret

What I am struggling with is how this translates to the example provided within the documentation that I have provided below..

Upon setting the customMetaDataTypeApiName private string to my metadata api name, I see the fields appear in the auth provider configuration page.

However, in the flow I am working with, there does not appear to be a need for the initiate method, but it is required. Am I misunderstanding Oauth, does a user need to log in to receive a token back? Or can I just pass the clientid, clientsecret, and grant type and received a response?

I tried getting the token response by an HTTP GET Request, but I am returned with a 403 forbidden error.

15:57:02:002 CALLOUT_REQUEST [5]|System.HttpRequest[Endpoint=https://myendpoint.com/token?grant_type=oauth2&client_id=myClientId12345&client_secret=lknvdas898-89fdn-fdnfdb, Method=GET]

  global class Concur extends Auth.AuthProviderPluginClass {

  // Use this URL for the endpoint that the 
  // authentication provider calls back to for configuration.
  public String redirectUrl; 
  private String key;
  private String secret;

  // Application redirection to the Concur website for 
  // authentication and authorization.
  private String authUrl;  

  // URI to get the new access token from concur using the GET verb.
  private String accessTokenUrl; 

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

  // Api URL to access the user in Concur
  private String userAPIUrl; 

  // Version of the user api URL to access data from Concur
  private String userAPIVersionUrl; 

  global String getCustomMetadataType() {
      return customMetadataTypeApiName;
  } 

  global PageReference initiate(Map<string,string> 
    authProviderConfiguration, String stateToPropagate) 
    { 
        authUrl = authProviderConfiguration.get('Auth_Url__c'); 
        key = authProviderConfiguration.get('Key__c'); 

        // Here the developer can build up a request of some sort. 
        // Ultimately, they return a URL where we will redirect the user. 
        String url = authUrl + '?client_id='+ key +'&scope=USER,EXPRPT,LIST&redirect_uri='+ redirectUrl + '&state=' + stateToPropagate; 
        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 
        key = authProviderConfiguration.get('Key__c'); 
        secret = authProviderConfiguration.get('Secret__c'); 
        accessTokenUrl = authProviderConfiguration.get('Access_Token_Url__c'); 

        Map<String,String> queryParams = state.queryParameters; 
        String code = queryParams.get('code'); 
        String sfdcState = queryParams.get('state'); 

        HttpRequest req = new HttpRequest(); 
        String url = accessTokenUrl+'?code=' + code + '&client_id=' + key + 
        '&client_secret=' + secret; 
        req.setEndpoint(url); 
        req.setHeader('Content-Type','application/xml'); 
        req.setMethod('GET'); 

        Http http = new Http(); 
        HTTPResponse res = http.send(req); 
        String responseBody = res.getBody(); 
        String token = getTokenValueFromResponse(responseBody, 'Token', null); 

        return new Auth.AuthProviderTokenResponse('Concur', token, 
        'refreshToken', sfdcState); 
    } 

    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/xml'); 
        req.setMethod('GET'); 

        Http http = new Http(); 
        HTTPResponse res = http.send(req); 
        String responseBody = res.getBody(); 
        String id = getTokenValueFromResponse(responseBody, 
        'LoginId',userAPIVersionUrl); 
        String fname = getTokenValueFromResponse(responseBody, 
        'FirstName', userAPIVersionUrl); 
        String lname = getTokenValueFromResponse(responseBody, 
        'LastName', userAPIVersionUrl); 
        String flname = fname + ' ' + lname; 
        String uname = getTokenValueFromResponse(responseBody, 
        'EmailAddress', userAPIVersionUrl); 
        String locale = getTokenValueFromResponse(responseBody, 
        'LocaleName', userAPIVersionUrl); 
        Map<String,String> provMap = new Map<String,String>(); 
        provMap.put('what1', 'noidea1'); 
        provMap.put('what2', 'noidea2'); 
        return new Auth.UserData(id, fname, lname, flname, 
        uname, 'what', locale, null, 'Concur', null, provMap); 
    } 

    private String getTokenValueFromResponse(String response, 
    String token, String ns) 
    { 
        Dom.Document docx = new Dom.Document(); 
        docx.load(response); 
        String ret = null; 

        dom.XmlNode xroot = docx.getrootelement() ; 
        if(xroot != null){ ret = xroot.getChildElement(token, ns).getText(); 
        } 
    return ret; 
    } 

}

Best Answer

I have not created one and when I go to create one and I am provided a list of pre-defined provider types, none of which are my providers.

That's when you need to create an External Authentication Provider, if the ones listed while creating an Auth. Provider does not exist.

For apps that don’t support OpenID Connect, Salesforce provides an Apex Auth.AuthProviderPluginClass abstract class to create a custom authentication provider.

And that you have now already created one by following the steps as outlined in the documentation.


Now for your questions:

However, in the flow I am working with, there does not appear to be a need for the initiate method, but it is required

There is always a need of initiate() method. Because by configuring an External Authentication Provider, you want your Salesforce Users to login to Salesforce by using the credentials as provided by your Authentication Provider, you will first need to redirect your Users to the Provider for authentication. This method is specifically used for that purpose.

Am I misunderstanding Oauth, does a user need to log in to receive a token back?

You are misunderstanding the OAuth flow a bit. In an OAuth flow, a User always needs to authenticate themselves first in order to get a token. And once authorized, then they can use the token for subsequent calls.

Or can I just pass the clientid, clientsecret, and grant type and received a response?

Only after you complete the authentication step, receive the token and then utilize the token for making the subsequent calls.

what do I need to do to work around the required initiate method

You don't need a workaround here. What you need is to configure the authentication URL of your External Authentication Provider. Once this is configured, you should be all good.