LINE Login V2.1 では Open ID Connect がサポートされたようですが、うまくログイン出来ていません。。
成功したか方がいれば教えてください!
~ 2017/10/11 現在
- Salesforce ではソーシャルアカウントの認証で Salesforce にログインするための機能として、外部認証プロバイダという設定項目が提供されています。
- 今回は LINE Login を使って、 LINE アカウントで Salesforce にログインする方法を試してました。
- 外部認証プロバイダの設定では、Facebook や Twitter といった世界的に主だった SNS や OpenID Connect 対応サービスには比較的簡単に認証設定が追加できるようになっています。しかし、現在のところ - LINE は非対応のため独自に認証プロバイダプラグインや登録ハンドラーを作成する必要があります。
- では、早速作成してみましょう。
外部認証プロバイダとは?
https://help.salesforce.com/articleView?id=sso_authentication_providers.htm&language=ja&type=0
OAuth をサポートしているが OpenID Connect プロトコルをサポートしていないサービスプロバイダ
https://help.salesforce.com/articleView?id=sso_provider_plugin_custom.htm&language=ja
#外部認証プロバイダの作成
##カスタム項目の作成
- LINE アカウントから取得した値の格納先となるカスタム項目をユーザーオブジェクトに作成します。
-
設定 > ビルド > カスタマイズ > ユーザー > 項目
から以下の 3 項目を作成しておきます。
-
設定 > ビルド > カスタマイズ > ユーザー > ページレイアウト
で作成した 3 項目をページレイアウトに追加しておきます。
##Apex クラスの作成
- 以下2つの Apex クラスを作成します。
LineAUthProviderPlugin.apxc
global class LineAuthProviderPlugin extends Auth.AuthProviderPluginClass {
private static final String FIRST_NAME = 'N/A';
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 = 'LINE';
private static final String PROVIDER_DEVELOPER_NAME = 'line';
global String getCustomMetadataType() {
// Make sure to create the Custom Metadata in advance.
// # If you are not-Engish user and getting an error sayin this Metadata is invalid,
// change your language to English and retry.
return 'LineAuthMetadata__mdt';
}
global PageReference initiate(Map<string,string> authProviderConfiguration, String stateToPropagate) {
String url = 'https://access.line.me/dialog/oauth/weblogin'
+ '?response_type=' + 'code'
+ '&client_id=' + authProviderConfiguration.get('ChannelId__c')
+ '&redirect_uri=' + EncodingUtil.urlEncode(authProviderConfiguration.get('RedirectUri__c'), 'UTF-8')
+ '&state=' + stateToPropagate;
return new PageReference(url);
}
global Auth.AuthProviderTokenResponse handleCallback(Map<string,string> authProviderConfiguration, Auth.AuthProviderCallbackState callbackState) {
// Create a reuqest for Line user token.
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/v2/oauth/accessToken');
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');
return new Auth.AuthProviderTokenResponse('Line', token, refreshToken, state);
}
global Auth.UserData getUserInfo(Map<string,string> authProviderConfiguration, Auth.AuthProviderTokenResponse response) {
// Create a reuqest for Line user profiles.
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();
// Since Line does not provide user's email address, we will create a temporary one.
// - The email address has to be unique so we can also use it as an username.
// - If the user is authrized as an internal user, use the org's domain for the email.
// If the user is authrized as an external user, use the community's domain.
String email = getValueFromResponse(responseBody, 'userId').hashCode() + '@';
if (String.isEmpty(Site.getSiteId())) {
email += Url.getSalesforceBaseURL().toExternalForm().substringAfter('//');
} else {
email += Site.getDomain();
}
// Set LINE sepecific attributes for creating and updating User record in org.
Map<String,String> attributeMap = new Map<String,String>();
attributeMap .put('displayName', getValueFromResponse(responseBody, 'displayName'));
attributeMap .put('pictureUrl', getValueFromResponse(responseBody, 'pictureUrl'));
if (!String.isEmpty(authProviderConfiguration.get('ProfileId__c')))
attributeMap .put('profileId', authProviderConfiguration.get('ProfileId__c'));
// Return the
return new Auth.UserData(getValueFromResponse(responseBody, 'userId'), // identifier
FIRST_NAME, // firstName
LAST_NAME, // lastName
FULL_NAME, // fullName
email, // email
LINK, // link
email, // userName
LOCALE, // locale
PROVIDER, // provider
null, //siteLoginUrl
attributeMap); // attributeMap
}
@RemoteAction
global static String getSsoUrl() {
// Set the Community Base Url if it is community access, or set salesforce base Url if it is internal user access.
String returnUrl = Site.getBaseUrl();
if (String.isEmpty(returnUrl)) {
returnUrl = Url.getSalesforceBaseURL().toExternalForm();
}
// If the login user already has LINE account linked and has no LINE user Id,
// generate sso Url for the user to re-login so user info gets updated.
Id userId = UserInfo.getUserId();
List<ThirdPartyAccountLink> accountLinks = [SELECT Id, UserId, Provider FROM ThirdPartyAccountLink WHERE UserId =: userId AND Provider =: PROVIDER_DEVELOPER_NAME];
User u = [SELECT Id, LineId__c FROM User WHERE Id =: userId];
if (!accountLinks.isEmpty() && String.isEmpty(u.LineId__c)) {
returnUrl = 'https://login.salesforce.com/services/auth/sso/' + UserInfo.getOrganizationId() + '/' + PROVIDER_DEVELOPER_NAME;
if (!String.isEmpty(Site.getSiteId())) {
returnUrl += '?community=' + EncodingUtil.urlEncode(Site.getBaseUrl(), 'UTF-8');
}
}
return returnUrl;
}
private String getValueFromResponse(String response, String key) {
Map<String, Object> responseMap = (Map<String, Object>)JSON.deserializeUntyped(response);
return (String)responseMap.get(key);
}
}
LineUserRegistrationHandler.apxc
global class LineUserRegistrationHandler 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){
List<User> userList = [SELECT Id FROM User WHERE LineId__c =: data.identifier AND IsActive = true];
User u = userList.isEmpty() ? new User() : userList[0];
// Initialize User sobject with the UserData
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;
// Set Line-sepecific attributes
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];
if (!network.OptionsSelfRegistrationEnabled) throw new RegHandlerException('Self registration feature has to be enabled.');
// If the user is an external user, a related Account and a Contact need to be created.
Account a = new Account();
if (isPersonAccountEnabled()) {
// If Person Account is enabled in the org, create a person account
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){
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');
update(u);
}
private boolean isPersonAccountEnabled() {
return Schema.sObjectType.Account.fields.getMap().containsKey('isPersonAccount');
}
}
##カスタムメタデータ型の作成
-
設定 > ビルド > 開発 > カスタムメタデータ型
から以下のメタデータ型を作成する。
- 作成したカスタムメタデータ型にカスタム項目を追加する。
##認証プロバイダを作成する
表示言語の変更
###認証プロバイダの設定
-
設定 > 管理 > セキュリティのコントロール > 認証プロバイダ > 新規
から以下の通り設定します。- Channel ID と Client Secret は LINE Login の Developer Page から取得したものを設定してください。
- Profile ID には、ユーザーがログイン時に割り当てられるプロファイルを指定します。今回はこの組織の Chatter External User の ID を設定しています。
- Execute As にはシステム管理者ユーザーを割り当てています。
- 保存後、各URLが表示されるので Callback URL を Redirect URL にコピーし保存し直します。
- LINE Login の Technical configuration にも Callback URL をコピーしておきます。
##リモートサイトの追加
-
設定 > 管理 > セキュリティのコントロール > リモートサイトの設定 > 新規
から以下の通り設定します。
#外部認証プロバイダの利用
##接続テスト
- 作成した認証プロバイダの
テスト専用初期化 URL
にアクセスし、LINE にログインすると正しく認証出来たか確認出来ます。 - 認証に成功した場合は以下のXMLが表示されます。
response
<user xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<full_name>N/A</full_name>
<provider>LINE</provider>
<org_id>00Dhfbl8e23hfilqf</org_id>
<link>N/A</link>
<last_name>N/A</last_name>
<id>U02easdffghjkqwertyui</id>
<portal_id>000000000000000</portal_id>
<locale xsi:nil="true"/>
<first_name>N/A</first_name>
<email>12345678@xxx-dev-ed.my.salesforce.com</email>
<username>12345678@xxx-dev-ed.my.salesforce.com</username>
</user>
##シングルサインオン
- ユーザーにLINEアカウントで組織にログインさせたい場合は、
シングルサインオン初期化 URL
にアクセスさせます。 - また、コミュニティにアクセスさせたい場合は、
シングルサインオン初期化 URL + '?community=' + (コミュニティのURL)
にアクセスさせます。- 例:
https://login.salesforce.com/services/auth/sso/00D1234567890asdfg/line?community=https://www.xxxxx.com/support
- コミュニティユーザーの場合、セルフレジスターの機能を有効化しておきましょう。
- 例:
##既存ユーザーにLINEアカウントを紐付ける
-
既存ユーザをリンクする URL
にアクセスさせます。 - この時、なぜかユーザーオブジェクトのカスタム項目が更新されないので、以下の Visualforce ページを作成し startUrl に指定する事で再度 SSO させる事にしました。
- 例:
https://login.salesforce.com/services/auth/link/00D1234567890asdfg/line?startURL=%2Fapex%2FRedirectToSso
- ※リンクするプロファイルに、Visualforceページへの権限不要が必要
- 例:
RedirectToSso.vfp
<apex:page showHeader="false" standardStylesheets="false" controller="LineAuthProviderPlugin">
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<div id="wrapper">
<div style="font-size: 24px;">Please wait...</div>
</div>
<script>
(function() {
getSsoUrl()
.then(function(ssoUrl) {
if (ssoUrl) window.location.href = ssoUrl;
})
.catch(function(err) {
alert(err.message);
});
})();
var REMOTEACTION_OPTIONS = {buffer: true, escape: true, timeout: 120000};
function getSsoUrl() {
return new Promise(function(resolve, reject) {
Visualforce.remoting.Manager.invokeAction(
'{!$RemoteAction.LineAuthProviderPlugin.getSsoUrl}',
function (result, event) {
if (event.status) resolve(result);
else reject(event);
}, this.REMOTEACTION_OPTIONS);
});
}
</script>
<style>
#wrapper {
position: absolute;
top: 50vh;
left: 50vw;
transform: translate(-50%, -50%);
text-align: center;
}
</style>
</apex:page>