Edited at

Salesforce カスタム認証プロバイダプラグインの作成 及び LINE アカウントで Community Cloud に SSO ログインする

みなさん、こんにちは!

あっちこっちのSalesforceコミュニティイベントを参加していますが、投稿ははじめてです。@takahito0508 さんに誘ってもらい、今年のアドベントカレンダーに参加しました。どんな内容で良いのかを悩んでいた最後、SSO関連にしようかなと思いました。

当記事は Salesforce Platform Advent Calendar 2018 - Day 13 の投稿になります。


はじめに

Salesforceの製品群の中、Community Cloudは外部向けカスタマサービスサイトの位置づけとなります。企業内部の情報共有や、外部ユーザーとやり取りのメッセージ(Chatter)などの機能があります。さらに、TwitterやFacebookなどソーシャルアカウントと連携してSalesforceにログインするには、集客効果も考えられます。しかし、デフォルトにアメリカで主流なSNSのプロバイダしか提供されていません。LINEとWeChatのような地域限定のSNSに対し、カスタム認証プロバイダを作成する必要です。


ユーザカスタム項目の新規作成

LINEアカウントの情報を格納するため、ユーザに以下のカスタム項目を追加します。


  • Line ID

  • Line Profile Image URL

  • Line User Name

image.png


LINE Social API でカスタム認証プロバイダプラグインクラス作成

Apexでカスタム認証プロバイダクラスを作成するには、Auth.AuthProviderPluginClassを拡張して以下の関数を実装する必要です。


  • getCustomMetadataType()

  • initiate(authProviderConfiguration, stateToPropagate)

  • handleCallback(authProviderConfiguration, callbackState)

  • getUserInfo(authProviderConfiguration, response)

LINE Social APIのバージョンは v2.1、ソースコードは以下となります。


LineAuthProviderPluginV21.cls

global class LineAuthProviderPluginV21 extends Auth.AuthProviderPluginClass {

private static final String FIRST_NAME = 'LineLogin';
private static final String LAST_NAME = 'N/A';
private static final String FULL_NAME = 'N/A';
private static final String LINK = 'N/A';
private static final String LOCALE = null;
private static final String PROVIDER = 'LINEV21';
private static final String PROVIDER_DEVELOPER_NAME = 'linev21';
private String id_token = '';

global String getIdToken() {
return id_token;
}

global void setIdToken(String id_token) {
this.id_token = id_token;
}

global String getCustomMetadataType() {
return 'LineAuthMetadata__mdt';
}

global PageReference initiate(Map<string,string> authProviderConfiguration,
String stateToPropagate) {
String url = 'https://access.line.me/oauth2/v2.1/authorize'
+ '?response_type=' + 'code'
+ '&client_id=' + authProviderConfiguration.get('ChannelId__c')
+ '&redirect_uri='
+ EncodingUtil.urlEncode(authProviderConfiguration.get('RedirectUri__c'), 'UTF-8')
+ '&state=' + stateToPropagate
+ '&scope=' + 'openid%20profile%20email';
return new PageReference(url);
}

global Auth.AuthProviderTokenResponse handleCallback(Map<string,string>
authProviderConfiguration, Auth.AuthProviderCallbackState callbackState) {
Map<String,String> queryParams = callbackState.queryParameters;
String code = queryParams.get('code');
String state = queryParams.get('state');

HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.line.me/oauth2/v2.1/token');
req.setHeader('Content-Type','application/x-www-form-urlencoded');
req.setMethod('POST');
req.setBody('grant_type=' + 'authorization_code'
+ '&code=' + code
+ '&client_id=' + authProviderConfiguration.get('ChannelId__c')
+ '&client_secret=' + authProviderConfiguration.get('ClientSecret__c')
+ '&redirect_uri='
+ EncodingUtil.urlEncode(authProviderConfiguration.get('RedirectUri__c'),
'UTF-8'));

Http http = new Http();
HTTPResponse res = http.send(req);
String responseBody = res.getBody();
String token = getValueFromResponse(responseBody, 'access_token');
String refreshToken = getValueFromResponse(responseBody, 'refresh_token');
Object expires = getObjectValueFromResponse(responsebody, 'expires_in');
id_token = getValueFromResponse(responseBody, 'id_token');

return new Auth.AuthProviderTokenResponse('Linev21', token, refreshToken, state);
}

global Auth.UserData getUserInfo(Map<string,string> authProviderConfiguration,
Auth.AuthProviderTokenResponse response) {
/**
* ユーザープロファイル取得のAPIはV2までしかバージョンアップされていなく、
* V2.1の場合V2のレスポンスから取得する必要がありません。
* ただし、V2のAPIを呼び出さないとSSOログインする際にSalesforceのメンテナンス画面が
* 表示され、LINE SSOユーザーアカウントが登録されません。
*/

HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.line.me/v2/profile');
req.setHeader('Authorization', 'Bearer ' + response.oauthToken);
req.setMethod('GET');

Http http = new Http();
HTTPResponse res = http.send(req);
String responseBody = res.getBody();

if (id_token == '') {
return null;
}

String[] rets = id_token.split('\\.');
String payload = EncodingUtil.base64Decode(rets[1]).toString();
Map<String, Object> payload_map =
(Map<String, Object>)JSON.deserializeUntyped(payload);
String userId = (String)payload_map.get('sub');
String email = (String)payload_map.get('email');
String lastName = (String)payload_map.get('name');
String userName = userId.hashCode() + '@';
if (String.isEmpty(Site.getSiteId())) {
userName += Url.getSalesforceBaseURL().toExternalForm().substringAfter('//');
} else {
userName += Site.getDomain();
}

Map<String,String> attributeMap = new Map<String,String>();
attributeMap.put('displayName', (String)payload_map.get('name'));
attributeMap.put('pictureUrl', (String)payload_map.get('picture'));
if (!String.isEmpty(authProviderConfiguration.get('ProfileId__c')))
attributeMap.put('profileId', authProviderConfiguration.get('ProfileId__c'));

return new Auth.UserData(userId,
FIRST_NAME,
lastName,
FULL_NAME,
email,
LINK,
userName,
LOCALE,
PROVIDER,
null,
attributeMap);
}

private String getValueFromResponse(String response, String key) {
Map<String, Object> responseMap =
(Map<String, Object>)JSON.deserializeUntyped(response);
return (String)responseMap.get(key);
}

private Object getObjectValueFromResponse(String response, String key) {
Map<String, Object> responseMap =
(Map<String, Object>)JSON.deserializeUntyped(response);
return responseMap.get(key);
}
}



外部認証プロバイダの登録ハンドラクラスの実装

登録ハンドラクラスを作成するには、インターフェース Auth.RegistrationHandler を実装して以下の関数を実装する必要です。


  • createUser(portalId, userData)

  • updateUser(userId, portalId, userData)


LineUserRegistrationHandlerV21.cls

global class LineUserRegistrationHandlerV21 implements Auth.RegistrationHandler {

private static final String TIME_ZONE = 'Asia/Tokyo';
private static final String LANGUAGE = 'ja';
private static final String LOCALE = 'ja_JP';
private static final String EMAIL_ENCODE = 'UTF-8';
private static final String ACCOUNT_NAME = 'LINE User';

class RegHandlerException extends Exception {}

global User createUser(Id portalId, Auth.UserData data){
System.debug('Method createUser was called.');

List<User> userList = [SELECT Id FROM User WHERE LineId__c =: data.identifier
AND IsActive = true];
User u = userList.isEmpty() ? new User() : userList[0];

u.LastName = data.lastName;
u.FirstName = data.firstName;
u.Email = data.email;
u.Username = data.username;
u.Alias = data.username.substring(0, 8);
u.ProfileId = data.attributeMap.get('profileId');
u.TimeZoneSidKey = TIME_ZONE;
u.LanguageLocaleKey = LANGUAGE;
u.LocaleSidKey = LOCALE;
u.EmailEncodingKey = EMAIL_ENCODE;

u.LineId__c = data.identifier;
u.LineUserName__c = data.attributeMap.get('displayName');
u.LineProfileImageURL__c = data.attributeMap.get('pictureUrl');

String siteId = System.Site.getSiteId();
if(String.isEmpty(u.Id) && !String.isEmpty(siteId)) {
Site site = [SELECT Id, MasterLabel, UrlPathPrefix
FROM Site
WHERE Id =: siteId LIMIT 1];
Network network = [SELECT Id, SelfRegProfileId, OptionsSelfRegistrationEnabled
FROM Network
WHERE Name =: site.MasterLabel AND UrlPathPrefix =:site.UrlPathPrefix LIMIT 1];

Account a = new Account();
if (isPersonAccountEnabled()) {
RecordType personAccountRecordType = Database.query('SELECT Id FROM RecordType '
+ 'WHERE SobjectType = \'Account\' AND IsPersonType = true LIMIT 1');
a.put('RecordTypeId', personAccountRecordType.Id);
a.put('LastName', data.lastName);
a.put('FirstName', data.firstName);
a.put('PersonEmail', data.email);
insert a;
} else {
a.name = data.fullname;
insert a;
Contact c = new Contact();
c.AccountId = a.Id;
c.FirstName = data.firstName;
c.LastName = data.lastName;
c.Email = data.email;
insert(c);
}
u.ProfileId = network.SelfRegProfileId;
u.contactId = [SELECT Id FROM Contact WHERE accountId =: a.Id][0].Id;
u.CommunityNickname = data.attributeMap.get('displayName');
}
return u;
}

global void updateUser(Id userId, Id portalId, Auth.UserData data){
System.debug('Method updateUser was called.');

User u = [SELECT Id, ContactId, LineId__c, LineUserName__c, LineProfileImageURL__c
FROM User WHERE Id =: userId];
u.LineId__c = data.identifier;
u.LineUserName__c = data.attributeMap.get('displayName');
u.LineProfileImageURL__c = data.attributeMap.get('pictureUrl');

u.LastName = data.lastName + '.V21.Update01';
u.FirstName = data.firstName;
u.Email = data.email + '.V21.Update01';

update(u);
}

private boolean isPersonAccountEnabled() {
return Schema.sObjectType.Account.fields.getMap().containsKey('isPersonAccount');
}
}



外部認証プロバイダの設定


LINEログインプロバイダー & チャンネル作成

LINE SSO ログインするには、LINE Developersでプロバイダーとチャンネルの作成が必要です。

image.png

image.png


メール取得権限の申請

image.png

image.png


カスタムメタデータ型の作成

カスタムプロバイダとハンドルに必要な情報を格納するため、カスタムメタデータ型「LineAuthMetadata」を作成し、以下のカスタム項目を新規作成します。


  • Channel ID:LINEログインのチャンネルの識別子「Channel ID」を格納

  • Client Secret:LINEログインのチャンネルの秘密鍵「Channel Secret」を格納

  • Profile ID: SFDC外部ユーザーのプロファイルIDを格納(例:Customer Community User)

  • Redirect URI:SFDC外部認証プロバイダのCallback URLを格納(例:https://login.salesforce.com/services/authcallback/00D6F0000028GOLUA5/lineV21)

image.png


SFDC外部認証プロバイダの新規作成

必要な項目を入力して保存します。

※ 現時点 (2018/12/13) の最新バージョンでも英語版に切り替えないと保存時にエラーになります。

image.png


LINEログインチャンネルCallback URLの更新・公開

LINEログインチャンネルのCallback URLをSFDC外部認証プロバイダのCallback URLに更新し、チャンネルを公開します。

image.png


カスタマサービスサイト作成


コミュニティの有効化と新規作成

コミュニティ設定でコミュニティの有効化にして、コミュニティを新規作成します。

image.png


管理設定


メンバー設定

コミュニティワークスペースの管理 → メンバーで顧客用のプロファイルCustomer Community Userを選択済みプロファイルに追加します。

image.png


ログインページの認証プロバイダの設定

ログイン&登録 → ログインページ設定の「LINEV21」にチェックを入れます。

image.png


リモートサイトの設定

LINEのAPIサイトをSFDCのリモートサイトに追加する必要です。

image.png


ログインしてみる


顧客カスタマサービスサイトログイン

image.png


LINEアカウントログイン画面

image.png


権限確認

image.png


ログイン後のトップページ

image.png


さいごに

OAuth、OpenIDの流れを理解できたら、Salesforce のカスタム認証プロバイダの開発に非常に役立ちます。Apex では基本の枠をすでに提供してくれました。インターフェースとクラスを拡張して各関数にAPI関連のコードを書き込んで連携の形にするだけです。ただ、LINEのようなサービス提供側は連携APIのバージョンアップによって新しい機能を使いたい場合、新たなプロバイダを作成する必要です。

今回は、LINEと連携のプロバイダを作成しました。WeChatの場合も同じやり方で作成できます。以下のリンク先は中国語のページとなりますが、ソースコードをご覧いただければ、どんな仕組を理解できます。ご興味ある方は是非一度ご確認をいただければと思います。

https://www.cnblogs.com/panxuejun/p/6094711.html(OAuth2がAouth2に間違えている)

また、WeChatのWebサイトアプリケーション作成のサイトは英語版もあります。

https://open.weixin.qq.com/cgi-bin/frame?t=home/web_tmpl&lang=en

ご参考になりましたら幸いでございます。

【参考】

1. Salesforce AuthProviderPluginClass クラス

https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_class_Auth_AuthProviderPluginClass.htm#apex_class_Auth_AuthProviderPluginClass

2. Salesforce RegistrationHandler クラス

https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_auth_plugin.htm#apex_auth_plugin

3. LINE Social API リファレンス (最新v2.1)

https://developers.line.biz/ja/reference/social-api/