3

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

}
  • 1
    On this statement -- [but I cannot configure the clientid, client secret, or access token url], are you saying that when you created an Auth Provider, you don't see an option to provide these values? Can you post a screenshot for the same? Also while creating an Auth Provider, what was the Provider Type you had selected? – Jayant Das Dec 7 '18 at 21:24
  • Just updated this as you commented, upon setting the customMetadataTypeApiName string to my meta data API name, the field I added to the meta data type now show up in the auth provider configuration screen. – S.B. Dec 7 '18 at 21:26
  • 1
    So is your question all about this statement -- [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?]? – Jayant Das Dec 7 '18 at 21:42
  • My core question now seems to be... is my request wrong, if not, what do I need to do to work around the required initiate method. – S.B. Dec 7 '18 at 21:46
  • In summary, you need to configure the authentication url for your external provider in the initiate method. I have added more details in the answer, hope it helps. – Jayant Das Dec 7 '18 at 22:03
1

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.

  • 1
    Thank you. I really appreciate the detailed breakdown on this. You drilled the point in on "users always need to authenticate themselves first". The way I was presented information indicated to me there was no user authentication needed, but in fact there is. – S.B. Dec 7 '18 at 22:10
  • Glad that it helped you. Understanding OAuth is always confusing. – Jayant Das Dec 7 '18 at 22:50
  • I have a follow up question on this. In a subsequent question: I asked about and was able to make a OAuth Client Credentials flow callout and receive the token back, I then am I able to use the token to call to the REST API to make authenticated callouts You stated that the Instantiate method was used to log the user in, but in the client credentials flow, there is no user login. Do I need a URL that just redirects the user back to salesforce? My end goal is to create an authentication provider that I can use with an External Data Source. If this is better as a new question, let me know. – S.B. Dec 11 '18 at 20:41
  • While this will classify as a different question, but here are some pointers. So the initiate method redirects to the authentication provider's authentication URL. Even in case of client credentials, you will still need to authenticate the user using some URL. You will need to handle the return value from this method in the handleCallback. – Jayant Das Dec 11 '18 at 21:03

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.