カスタム認証プロバイダプラグインの作成
Apex を使用して、Salesforce へのシングルサインオン (SSO) 用に OAuth ベースのカスタム認証プロバイダプラグインを作成できます。
シングルサインオン (SSO) は、ユーザが 1 回のログインと 1 セットのログイン情報で複数のアプリケーションにアクセスできるようにする認証方法です。たとえば、ユーザが組織にログインすると、自動的にアプリケーションランチャーからすべてのアプリケーションにアクセスできます。ユーザを認証するために外部 ID プロバイダを信頼するように Salesforce 組織を設定できます。または、組織を使用して認証するように外部アプリケーションを設定することもできます。Salesforce は標準でシングルサインオン用の外部認証プロバイダとして Facebook や Google、LinkedIn などをサポートし、OpenID Connect プロトコルを実装するサービスプロバイダもサポートしています。Apex を使用してプラグインを作成することで、OAuth ベースの独自の認証プロバイダを追加できます。また、ユーザは、Salesforce 組織で Salesforce 以外のアプリケーションにすでに使用している SSO ログイン情報を使用できます。
Apex クラスを作成する前に、認証プロバイダのカスタムメタデータ型レコードを作成します。詳細は、「カスタム外部認証プロバイダの作成」を参照してください。
1HttpRequest req = new HttpRequest();
2String url = 'https://login.salesforce.com/';
3req.setEndpoint(url);
4req.setMethod('GET');
5Http http = new Http();
6HTTPResponse res = http.send(req);サンプルクラス
この例では、抽象クラス Auth.AuthProviderPluginClass を拡張して、Concur という外部認証プロバイダを設定します。次の順序で、サンプルクラスとサンプルテストクラスを作成します。
- Concur
- ConcurTestStaticVar
- MockHttpResponseGenerator
- ConcurTestClass
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 String token = getTokenValueFromResponse(responseBody, 'Token', null);
52
53 return new Auth.AuthProviderTokenResponse('Concur', accessToken, 'refreshToken', sfdcState);
54 //don’t hard-code the refresh token value!
55 }
56
57
58 global Auth.UserData getUserInfo(Map<string,string> authProviderConfiguration, Auth.AuthProviderTokenResponse response) {
59 //Here the developer is responsible for constructing an Auth.UserData object
60 String token = response.oauthToken;
61 HttpRequest req = new HttpRequest();
62 userAPIUrl = authProviderConfiguration.get('API_User_Url__c');
63 userAPIVersionUrl = authProviderConfiguration.get('API_User_Version_Url__c');
64 req.setHeader('Authorization', 'OAuth ' + token);
65 req.setEndpoint(userAPIUrl);
66 req.setHeader('Content-Type','application/xml');
67 req.setMethod('GET');
68
69 Http http = new Http();
70 HTTPResponse res = http.send(req);
71 String responseBody = res.getBody();
72 String id = getTokenValueFromResponse(responseBody, 'LoginId',userAPIVersionUrl);
73 String fname = getTokenValueFromResponse(responseBody, 'FirstName', userAPIVersionUrl);
74 String lname = getTokenValueFromResponse(responseBody, 'LastName', userAPIVersionUrl);
75 String flname = fname + ' ' + lname;
76 String uname = getTokenValueFromResponse(responseBody, 'EmailAddress', userAPIVersionUrl);
77 String locale = getTokenValueFromResponse(responseBody, 'LocaleName', userAPIVersionUrl);
78 Map<String,String> provMap = new Map<String,String>();
79 provMap.put('what1', 'noidea1');
80 provMap.put('what2', 'noidea2');
81 return new Auth.UserData(id, fname, lname, flname, uname,
82 'what', locale, null, 'Concur', null, provMap);
83 }
84
85 private String getTokenValueFromResponse(String response, String token, String ns) {
86 Dom.Document docx = new Dom.Document();
87 docx.load(response);
88 String ret = null;
89
90 dom.XmlNode xroot = docx.getrootelement() ;
91 if(xroot != null){
92 ret = xroot.getChildElement(token, ns).getText();
93 }
94 return ret;
95 }
96
97}サンプルテストクラス
次の例には、Concur のテストクラスが含まれています。
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('Token', null, null).addTextNode(OAUTH_TOKEN);
117 System.debug(doc.toXmlString());
118 // Create a fake response
119 HttpResponse res = new HttpResponse();
120 res.setHeader('Content-Type', 'application/xml');
121 res.setBody(doc.toXmlString());
122 res.setStatusCode(200);
123 return res;
124 }
125
126 }
127}