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

1global 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               global String getCustomMetadataType() {
14                   return customMetadataTypeApiName;
15               }
16       
17               global 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               global 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                 global 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 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    static testMethod void testInitiateMethod() {
39           String stateToPropogate = 'mocktestState';
40           Map<String,String> authProviderConfiguration = setupAuthProviderConfig();
41           Concur concurCls = new Concur();
42           concurCls.redirectUrl = authProviderConfiguration.get('Redirect_Url__c');
43           
44           PageReference expectedUrl =  new PageReference(authProviderConfiguration.get('Auth_Url__c') + '?client_id='+ 
45                                               authProviderConfiguration.get('Key__c') +'&scope=USER,EXPRPT,LIST&redirect_uri='+ 
46                                               authProviderConfiguration.get('Redirect_Url__c') + '&state=' + 
47                                               STATE_TO_PROPOGATE);
48           PageReference actualUrl = concurCls.initiate(authProviderConfiguration, STATE_TO_PROPOGATE);
49           System.assertEquals(expectedUrl.getUrl(), actualUrl.getUrl());
50       }
51    
52    static testMethod void testHandleCallback() {
53           Map<String,String> authProviderConfiguration = setupAuthProviderConfig();
54           Concur concurCls = new Concur();
55           concurCls.redirectUrl = authProviderConfiguration.get('Redirect_Url_c');
56
57           Test.setMock(HttpCalloutMock.class, new ConcurMockHttpResponseGenerator());
58
59           Map<String,String> queryParams = new Map<String,String>();
60           queryParams.put('code','code');
61           queryParams.put('state',authProviderConfiguration.get('State_c'));
62           Auth.AuthProviderCallbackState cbState = new Auth.AuthProviderCallbackState(null,null,queryParams);
63           Auth.AuthProviderTokenResponse actualAuthProvResponse = concurCls.handleCallback(authProviderConfiguration, cbState);
64           Auth.AuthProviderTokenResponse expectedAuthProvResponse = new Auth.AuthProviderTokenResponse('Concur', OAUTH_TOKEN, REFRESH_TOKEN, null);
65           
66           System.assertEquals(expectedAuthProvResponse.provider, actualAuthProvResponse.provider);
67           System.assertEquals(expectedAuthProvResponse.oauthToken, actualAuthProvResponse.oauthToken);
68           System.assertEquals(expectedAuthProvResponse.oauthSecretOrRefreshToken, actualAuthProvResponse.oauthSecretOrRefreshToken);
69           System.assertEquals(expectedAuthProvResponse.state, actualAuthProvResponse.state);
70           
71
72    }
73    
74    
75    static testMethod void testGetUserInfo() {
76           Map<String,String> authProviderConfiguration = setupAuthProviderConfig();
77           Concur concurCls = new Concur();
78                      
79           Test.setMock(HttpCalloutMock.class, new ConcurMockHttpResponseGenerator());
80
81           Auth.AuthProviderTokenResponse response = new Auth.AuthProviderTokenResponse(PROVIDER, OAUTH_TOKEN ,'sampleOauthSecret', STATE);
82           Auth.UserData actualUserData = concurCls.getUserInfo(authProviderConfiguration, response) ;
83           
84           Map<String,String> provMap = new Map<String,String>();
85           provMap.put('key1', 'value1');
86           provMap.put('key2', 'value2');
87                     
88           Auth.UserData expectedUserData = new Auth.UserData(LOGIN_ID, FIRST_NAME, LAST_NAME, FULL_NAME, EMAIL_ADDRESS,
89                          null, LOCALE_NAME, null, PROVIDER, null, provMap);
90          
91           System.assertNotEquals(expectedUserData,null);
92           System.assertEquals(expectedUserData.firstName, actualUserData.firstName);
93           System.assertEquals(expectedUserData.lastName, actualUserData.lastName);
94           System.assertEquals(expectedUserData.fullName, actualUserData.fullName);
95           System.assertEquals(expectedUserData.email, actualUserData.email);
96           System.assertEquals(expectedUserData.username, actualUserData.username);
97           System.assertEquals(expectedUserData.locale, actualUserData.locale);
98           System.assertEquals(expectedUserData.provider, actualUserData.provider);
99           System.assertEquals(expectedUserData.siteLoginUrl, actualUserData.siteLoginUrl);
100    }
101    
102    
103   // implementing a mock http response generator for concur 
104   public  class ConcurMockHttpResponseGenerator implements HttpCalloutMock {
105     public HTTPResponse respond(HTTPRequest req) {
106        String namespace = API_USER_VERSION_URL;
107        String prefix = 'mockPrefix';
108
109        Dom.Document doc = new Dom.Document();
110        Dom.XmlNode xmlNode =  doc.createRootElement('mockRootNodeName', namespace, prefix);
111        xmlNode.addChildElement('LoginId', namespace, prefix).addTextNode(LOGIN_ID);
112        xmlNode.addChildElement('FirstName', namespace, prefix).addTextNode(FIRST_NAME);
113        xmlNode.addChildElement('LastName', namespace, prefix).addTextNode(LAST_NAME);
114        xmlNode.addChildElement('EmailAddress', namespace, prefix).addTextNode(EMAIL_ADDRESS);
115        xmlNode.addChildElement('LocaleName', namespace, prefix).addTextNode(LOCALE_NAME);            
116        xmlNode.addChildElement('AccessToken', null, null).addTextNode(OAUTH_TOKEN);
117        xmlNode.addChildElement('RefreshToken', null, null).addTextNode(REFRESH_TOKEN);
118        System.debug(doc.toXmlString());
119        // Create a fake response
120        HttpResponse res = new HttpResponse();
121        res.setHeader('Content-Type', 'application/xml');
122        res.setBody(doc.toXmlString());
123        res.setStatusCode(200);
124        return res;
125    }
126   
127  }
128}