Create a Custom Authentication Provider Plug-in

You can use Apex to create a custom OAuth-based authentication provider plug-in for single sign-on (SSO) to Salesforce.

Out of the box, Salesforce supports several external authentication providers for single sign-on, including Facebook, Google, LinkedIn, and service providers that implement the OpenID Connect protocol. By creating a plug-in with Apex, you can add your own OAuth-based authentication provider. Your users can then use the SSO credentials they already use for non-Salesforce applications with your Salesforce orgs.

Before you create your Apex class, you create a custom metadata type record for your authentication provider. For details, see Create a Custom External Authentication Provider.

Sample Classes

This example extends the abstract class Auth.AuthProviderPluginClass to configure an external authentication provider called Concur. Build the sample classes and sample test classes in the following order.
  1. Concur
  2. ConcurTestStaticVar
  3. MockHttpResponseGenerator
  4. ConcurTestClass

The Auth.AuthProviderPluginClass class doesn't include a method for single logout. You can easily configure single logout in Setup. For steps, see Configure OpenID Connect Single Logout with Salesforce as the Relying Party in Salesforce Help. Alternatively, create custom methods for single logout.

Note

1public with sharing class Concur extends Auth.AuthProviderPluginClass {
2               
3               public String redirectUrl; // use this URL for the endpoint that the authentication provider calls back to for configuration
4               private String key;
5               private String secret;
6               private String authUrl;    // application redirection to the Concur website for authentication and authorization
7               private String accessTokenUrl; // uri to get the new access token from concur  using the GET verb
8               private String customMetadataTypeApiName; // api name for the custom metadata type created for this auth provider
9               private String userAPIUrl; // api url to access the user in concur
10               private String userAPIVersionUrl; // version of the user api url to access data from concur
11               
12       
13               public String getCustomMetadataType() {
14                   return customMetadataTypeApiName;
15               }
16       
17               public PageReference initiate(Map<string,string> authProviderConfiguration, String stateToPropagate) {
18                   authUrl = authProviderConfiguration.get('Auth_Url__c');
19                   key = authProviderConfiguration.get('Key__c');
20                   //Here the developer can build up a request of some sort
21                   //Ultimately they’ll return a URL where we will redirect the user
22                   String url = authUrl + '?client_id='+ key +'&scope=USER,EXPRPT,LIST&redirect_uri='+ redirectUrl + '&state=' + stateToPropagate;
23                   return new PageReference(url);
24                }
25        
26               public Auth.AuthProviderTokenResponse handleCallback(Map<string,string> authProviderConfiguration, Auth.AuthProviderCallbackState state ) {
27                   //Here, the developer will get the callback with actual protocol.  
28                   //Their responsibility is to return a new object called AuthProviderToken
29                   //This will contain an optional accessToken and refreshToken
30                   key = authProviderConfiguration.get('Key__c');
31                   secret = authProviderConfiguration.get('Secret__c');
32                   accessTokenUrl = authProviderConfiguration.get('Access_Token_Url__c');
33                   
34                   Map<String,String> queryParams = state.queryParameters;
35                   String code = queryParams.get('code');
36                   String sfdcState = queryParams.get('state');
37                   
38                   HttpRequest req = new HttpRequest();
39                   String url = accessTokenUrl+'?code=' + code + '&client_id=' + key + '&client_secret=' + secret;
40                   req.setEndpoint(url);
41                   req.setHeader('Content-Type','application/xml');
42                   req.setMethod('GET');
43                   
44                   Http http = new Http();
45                   HTTPResponse res = http.send(req);
46                   String responseBody = res.getBody();
47                   String accessToken = getTokenValueFromResponse(responseBody, 'AccessToken', null);
48                   //Parse access token value
49                   String refreshToken = getTokenValueFromResponse(responseBody, 'RefreshToken', null);
50                   //Parse refresh token value
51                   
52                   return new Auth.AuthProviderTokenResponse('Concur', accessToken, 'refreshToken', sfdcState);
53                   //don’t hard-code the refresh token value!
54                }
55    
56    
57                 public Auth.UserData  getUserInfo(Map<string,string> authProviderConfiguration, Auth.AuthProviderTokenResponse response) { 
58                     //Here the developer is responsible for constructing an Auth.UserData object
59                     String token = response.oauthToken;
60                     HttpRequest req = new HttpRequest();
61                     userAPIUrl = authProviderConfiguration.get('API_User_Url__c');
62                     userAPIVersionUrl = authProviderConfiguration.get('API_User_Version_Url__c');
63                     req.setHeader('Authorization', 'OAuth ' + token);
64                     req.setEndpoint(userAPIUrl);
65                     req.setHeader('Content-Type','application/xml');
66                     req.setMethod('GET');
67                     
68                     Http http = new Http();
69                     HTTPResponse res = http.send(req);
70                     String responseBody = res.getBody();
71                     String id = getTokenValueFromResponse(responseBody, 'LoginId',userAPIVersionUrl);
72                     String fname = getTokenValueFromResponse(responseBody, 'FirstName', userAPIVersionUrl);
73                     String lname = getTokenValueFromResponse(responseBody, 'LastName', userAPIVersionUrl);
74                     String flname = fname + ' ' + lname;
75                     String uname = getTokenValueFromResponse(responseBody, 'EmailAddress', userAPIVersionUrl);
76                     String locale = getTokenValueFromResponse(responseBody, 'LocaleName', userAPIVersionUrl);
77                     Map<String,String> provMap = new Map<String,String>();
78                     provMap.put('what1', 'noidea1');
79                     provMap.put('what2', 'noidea2');
80                     return new Auth.UserData(id, fname, lname, flname, uname,
81                          'what', locale, null, 'Concur', null, provMap);
82                }
83                
84                private String getTokenValueFromResponse(String response, String token, String ns) {
85                    Dom.Document docx = new Dom.Document();
86                    docx.load(response);
87                    String ret = null;
88
89                    dom.XmlNode xroot = docx.getrootelement() ;
90                    if(xroot != null){
91                       ret = xroot.getChildElement(token, ns).getText();
92                    }
93                    return ret;
94                }  
95    
96}

Sample Test Classes

The following example contains test classes for the Concur class.

1@IsTest
2public with sharing class ConcurTestClass {
3
4    private static final String OAUTH_TOKEN = 'testToken';
5    private static final String STATE = 'mocktestState';
6    private static final String REFRESH_TOKEN = 'refreshToken';
7    private static final String LOGIN_ID = 'testLoginId';
8    private static final String USERNAME = 'testUsername';
9    private static final String FIRST_NAME = 'testFirstName';
10    private static final String LAST_NAME = 'testLastName';
11    private static final String EMAIL_ADDRESS = 'testEmailAddress';
12    private static final String LOCALE_NAME = 'testLocalName';
13    private static final String FULL_NAME = FIRST_NAME + ' ' + LAST_NAME;
14    private static final String PROVIDER = 'Concur';
15    private static final String REDIRECT_URL = 'http://localhost/services/authcallback/orgId/Concur';
16    private static final String KEY = 'testKey';
17    private static final String SECRET = 'testSecret';
18    private static final String STATE_TO_PROPOGATE  = 'testState';
19    private static final String ACCESS_TOKEN_URL = 'http://www.dummyhost.com/accessTokenUri';
20    private static final String API_USER_VERSION_URL = 'http://www.dummyhost.com/user/20/1';
21    private static final String AUTH_URL = 'http://www.dummy.com/authurl';
22    private static final String API_USER_URL = 'www.concursolutions.com/user/api';
23
24    // in the real world scenario , the key and value would be read from the (custom fields in) custom metadata type record
25    private static Map<String,String> setupAuthProviderConfig () {
26            Map<String,String> authProviderConfiguration = new Map<String,String>();
27           authProviderConfiguration.put('Key__c', KEY);
28           authProviderConfiguration.put('Auth_Url__c', AUTH_URL);
29           authProviderConfiguration.put('Secret__c', SECRET);
30           authProviderConfiguration.put('Access_Token_Url__c', ACCESS_TOKEN_URL);
31           authProviderConfiguration.put('API_User_Url__c',API_USER_URL);
32           authProviderConfiguration.put('API_User_Version_Url__c',API_USER_VERSION_URL);
33           authProviderConfiguration.put('Redirect_Url__c',REDIRECT_URL);
34           return authProviderConfiguration;
35          
36    }
37
38    @IsTest
39    static void testInitiateMethod() {
40           String stateToPropogate = 'mocktestState';
41           Map<String,String> authProviderConfiguration = setupAuthProviderConfig();
42           Concur concurCls = new Concur();
43           concurCls.redirectUrl = authProviderConfiguration.get('Redirect_Url__c');
44           
45           PageReference expectedUrl =  new PageReference(authProviderConfiguration.get('Auth_Url__c') + '?client_id='+ 
46                                               authProviderConfiguration.get('Key__c') +'&scope=USER,EXPRPT,LIST&redirect_uri='+ 
47                                               authProviderConfiguration.get('Redirect_Url__c') + '&state=' + 
48                                               STATE_TO_PROPOGATE);
49           PageReference actualUrl = concurCls.initiate(authProviderConfiguration, STATE_TO_PROPOGATE);
50           Assert.areEqual(expectedUrl.getUrl(), actualUrl.getUrl());
51       }
52    
53    @IsTest
54    static void testHandleCallback() {
55           Map<String,String> authProviderConfiguration = setupAuthProviderConfig();
56           Concur concurCls = new Concur();
57           concurCls.redirectUrl = authProviderConfiguration.get('Redirect_Url_c');
58
59           Test.setMock(HttpCalloutMock.class, new ConcurMockHttpResponseGenerator());
60
61           Map<String,String> queryParams = new Map<String,String>();
62           queryParams.put('code','code');
63           queryParams.put('state',authProviderConfiguration.get('State_c'));
64           Auth.AuthProviderCallbackState cbState = new Auth.AuthProviderCallbackState(null,null,queryParams);
65           Auth.AuthProviderTokenResponse actualAuthProvResponse = concurCls.handleCallback(authProviderConfiguration, cbState);
66           Auth.AuthProviderTokenResponse expectedAuthProvResponse = new Auth.AuthProviderTokenResponse('Concur', OAUTH_TOKEN, REFRESH_TOKEN, null);
67           
68           Assert.areEqual(expectedAuthProvResponse.provider, actualAuthProvResponse.provider);
69           Assert.areEqual(expectedAuthProvResponse.oauthToken, actualAuthProvResponse.oauthToken);
70           Assert.areEqual(expectedAuthProvResponse.oauthSecretOrRefreshToken, actualAuthProvResponse.oauthSecretOrRefreshToken);
71           Assert.areEqual(expectedAuthProvResponse.state, actualAuthProvResponse.state);
72           
73
74    }
75    
76    
77    @IsTest
78    static void testGetUserInfo() {
79           Map<String,String> authProviderConfiguration = setupAuthProviderConfig();
80           Concur concurCls = new Concur();
81                      
82           Test.setMock(HttpCalloutMock.class, new ConcurMockHttpResponseGenerator());
83
84           Auth.AuthProviderTokenResponse response = new Auth.AuthProviderTokenResponse(PROVIDER, OAUTH_TOKEN ,'sampleOauthSecret', STATE);
85           Auth.UserData actualUserData = concurCls.getUserInfo(authProviderConfiguration, response) ;
86           
87           Map<String,String> provMap = new Map<String,String>();
88           provMap.put('key1', 'value1');
89           provMap.put('key2', 'value2');
90                     
91           Auth.UserData expectedUserData = new Auth.UserData(LOGIN_ID, FIRST_NAME, LAST_NAME, FULL_NAME, EMAIL_ADDRESS,
92                          null, LOCALE_NAME, null, PROVIDER, null, provMap);
93          
94           Assert.isNotNull(expectedUserData);
95           Assert.areEqual(expectedUserData.firstName, actualUserData.firstName);
96           Assert.areEqual(expectedUserData.lastName, actualUserData.lastName);
97           Assert.areEqual(expectedUserData.fullName, actualUserData.fullName);
98           Assert.areEqual(expectedUserData.email, actualUserData.email);
99           Assert.areEqual(expectedUserData.username, actualUserData.username);
100           Assert.areEqual(expectedUserData.locale, actualUserData.locale);
101           Assert.areEqual(expectedUserData.provider, actualUserData.provider);
102           Assert.areEqual(expectedUserData.siteLoginUrl, actualUserData.siteLoginUrl);
103    }
104    
105    
106   // implementing a mock http response generator for concur 
107   public with sharing class ConcurMockHttpResponseGenerator implements HttpCalloutMock {
108     public HTTPResponse respond(HTTPRequest req) {
109        String namespace = API_USER_VERSION_URL;
110        String prefix = 'mockPrefix';
111
112        Dom.Document doc = new Dom.Document();
113        Dom.XmlNode xmlNode =  doc.createRootElement('mockRootNodeName', namespace, prefix);
114        xmlNode.addChildElement('LoginId', namespace, prefix).addTextNode(LOGIN_ID);
115        xmlNode.addChildElement('FirstName', namespace, prefix).addTextNode(FIRST_NAME);
116        xmlNode.addChildElement('LastName', namespace, prefix).addTextNode(LAST_NAME);
117        xmlNode.addChildElement('EmailAddress', namespace, prefix).addTextNode(EMAIL_ADDRESS);
118        xmlNode.addChildElement('LocaleName', namespace, prefix).addTextNode(LOCALE_NAME);            
119        xmlNode.addChildElement('AccessToken', null, null).addTextNode(OAUTH_TOKEN);
120        xmlNode.addChildElement('RefreshToken', null, null).addTextNode(REFRESH_TOKEN);
121        System.debug(doc.toXmlString());
122        // Create a fake response
123        HttpResponse res = new HttpResponse();
124        res.setHeader('Content-Type', 'application/xml');
125        res.setBody(doc.toXmlString());
126        res.setStatusCode(200);
127        return res;
128    }
129   
130  }
131}