この文章は Salesforce 機械翻訳システムを使用して翻訳されました。詳細はこちらをご参照ください。
英語に切り替える

LoginDiscoveryHandler インターフェース

Salesforce では、ユーザ名とパスワード以外の確認方法に基づくユーザのログイン機能が提供されます。たとえば、メール、電話番号、または別の ID (統合 ID やデバイス識別子など) を使用してログインするようにユーザに要求できます。Login Discovery は、Customer Community、Customer Community Plus、External Identity、Partner Community、Partner Community Plus などのすべての外部ユーザライセンスで使用可能です。

名前空間

Auth

使用方法

インタビューベースのログイン用の Auth.LoginDiscoveryHandler を実装します。このハンドラは、入力された ID からユーザを検索し、Site.passwordlessLogin をコールして、使用するログイン情報 (メール、SMS など) を決定できます。または、ハンドラはユーザをログイン用のサードパーティ ID プロバイダにリダイレクトできます。このハンドラを使用した場合、ログインページにパスワード項目は表示されません。ただし、Site.passwordlessLogin を使用して、パスワードを要求できます。

ユーザの観点では、ユーザはログイン画面で ID を入力します。次に、PIN またはパスワードを入力してログインを完了します。または、SSO が有効になっている場合、ユーザはログインをスキップします。

例については、「LoginDiscoveryHandler の実装例」を参照してください。詳細は、『Salesforce External Identity Implementation Guide (Salesforce External Identity 実装ガイド)』を参照してください。

LoginDiscoveryHandler メソッド

LoginDiscoveryHandler のメソッドは次のようになります。

login(identifier, startUrl, requestAttributes)

メールや電話番号などの識別子が指定された外部ユーザのログインを行います。成功した場合、開始 URL で指定されたコミュニティページにユーザをリダイレクトします。

署名

public System.PageReference login(String identifier, String startUrl, Map<String,String>requestAttributes)

パラメータ

identifier
型: String
ログイン画面で外部ユーザが入力した識別子 (メールアドレスや電話番号など)。
startUrl
型: String
外部ユーザが要求したコミュニティページへのパス。ログインが成功すると、ユーザはこの場所にリダイレクトされます。
requestAttributes
型: Map<String,String>
ログインページにアクセスしたときのユーザのブラウザ状態に基づくログイン要求に関する情報。requestAttributes は、CommunityUrl、IpAddress、UserAgent、Platform、Application、City、Country、Subdivision の値を渡します。City、Country、Subdivision の値は IP 地理位置情報から取得されます。

戻り値

型: System.PageReference

ユーザがリダイレクトされるページの URL。

次に requestAttributes のサンプル応答を示します。

1CommunityUrl=http://my-developer-edition.mycompany.com:5555/discover
2IpAddress=55.555.0.0
3UserAgent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1 Safari/605.1.15
4Platform=Mac OSX
5Application=Browser
6City=San Mateo
7Country=United States
8Subdivision=California

LoginDiscoveryHandler の実装例

この Apex コード例は Auth.LoginDiscoveryHandler インターフェースを実装します。ログインページで提供された ID に応じて、ログインしているユーザのメールまたは電話番号が検証済みかどうかをチェックします。検証済みの場合 (Auth.VerificationMethod.EMAIL または Auth.VerificationMethod.SMS)、ID (ユーザのメールアドレスまたはモバイルデバイス) にチャレンジを送信します。検証ページでユーザがコードを正しく入力すると、ユーザは開始 URL で指定されたコミュニティページにリダイレクトされます。ユーザが検証されない場合、ユーザはログインのためのパスワードを入力する必要があります。また、ハンドラは users.size()==1 のコードによってメールアドレスと電話番号が一意であることも確認します。

パスワードなしのログインは検証済みの方法でのみ機能します。ユーザオブジェクトの検証状態は、ユーザリストビュー、レポート、API などを使用して確認できます。使用するソリューションで、ユーザに検証方法がない場合の処理が行われることを確認してください。このコード例では、パスワードに戻ります。

デフォルトの検出可能ログインハンドラは、ユーザが有効なメールアドレスまたは電話番号を入力したかを確認してからユーザを検証ページにリダイレクトします。無効な入力が行われた場合には、ハンドラはエラーを返します。この動作はユーザ列挙攻撃に対して脆弱であるため、必ずソリューションでこの攻撃を防げるようにします。たとえば、検証ページに似たダミーページを作成し、無効なユーザが入力されたらユーザをダミーページにリダイレクトすることができます。また、追加情報を与えないように、一般的なエラーメッセージを使用します。

メモ

discoveryResult 関数は Site.passwordlessLogin メソッドをコールし、指定された検証方法でユーザをログインします。getSsoRedirect 関数は、ユーザが SAML または認証プロバイダのどちらを使用してログインするかを参照します。実装に固有のロジックを追加して、この参照方法を操作します。

1global class AutocreatedDiscLoginHandler1535377170343 implements Auth.LoginDiscoveryHandler {
2
3global PageReference login(String identifier, String startUrl, Map<String, String> requestAttributes) {
4    if (identifier != null && isValidEmail(identifier)) {
5        // Search for user by email. 
6        List<User> users = [SELECT Id FROM User WHERE Email = :identifier AND IsActive = TRUE];
7        if (!users.isEmpty() && users.size() == 1) {
8            // User must have a verified email before using this verification method. 
9            // We cannot send messages to unverified emails. 
10            // You can check if the user's email verified bit set and add the 
11            // password verification method as fallback.
12            List<TwoFactorMethodsInfo> verifiedInfo = [SELECT HasUserVerifiedEmailAddress FROM TwoFactorMethodsInfo WHERE UserId = :users[0].Id];
13            if (!verifiedInfo.isEmpty() && verifiedInfo[0].HasUserVerifiedEmailAddress == true) {
14                // Use email verification method if the user's email is verified.
15                return discoveryResult(users[0], Auth.VerificationMethod.EMAIL, startUrl, requestAttributes);
16            } else {
17                // Use password verification method as fallback 
18                // if the user's email is unverified.
19                return discoveryResult(users[0], Auth.VerificationMethod.PASSWORD, startUrl, requestAttributes);
20            }
21        } else {
22            throw new Auth.LoginDiscoveryException('No unique user found. User count=' + users.size());
23        }
24    }
25    if (identifier != null) {
26        String formattedSms = getFormattedSms(identifier);
27        if (formattedSms != null) {
28            // Search for user by SMS. 
29            List<User> users = [SELECT Id FROM User WHERE MobilePhone = :formattedSms AND IsActive = TRUE];
30            if (!users.isEmpty() && users.size() == 1) {
31                // User must have a verified SMS before using this verification method. 
32                // We cannot send messages to unverified mobile numbers. 
33                // You can check if the user's mobile verified bit is set or add 
34                // the password verification method as fallback.
35                List<TwoFactorMethodsInfo> verifiedInfo = [SELECT HasUserVerifiedMobileNumber FROM TwoFactorMethodsInfo WHERE UserId = :users[0].Id];
36                if (!verifiedInfo.isEmpty() && verifiedInfo[0].HasUserVerifiedMobileNumber == true) {
37                    // Use SMS verification method if the user's mobile number is verified.
38                    return discoveryResult(users[0], Auth.VerificationMethod.SMS, startUrl, requestAttributes);
39                } else {
40                    // Use password verification method as fallback if the user's 
41                    // mobile number is unverified.
42                    return discoveryResult(users[0], Auth.VerificationMethod.PASSWORD, startUrl, requestAttributes);
43                }
44            } else {
45                throw new Auth.LoginDiscoveryException('No unique user found. User count=' + users.size());
46            }
47        }
48    }
49    if (identifier != null) {
50        // You can customize the code to find user via other attributes,
51        // such as SSN or Federation ID.
52    }
53    throw new Auth.LoginDiscoveryException('Invalid Identifier');
54}
55private boolean isValidEmail(String identifier) {
56    String emailRegex = '^[a-zA-Z0-9._|\\\\%#~`=?&/$^*!}{+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}$';
57    // source: http://www.regular-expressions.info/email.html 
58    Pattern EmailPattern = Pattern.compile(emailRegex);
59    Matcher EmailMatcher = EmailPattern.matcher(identifier);
60    if (EmailMatcher.matches()) { return true; }
61    else { return false; }
62}
63private String getFormattedSms(String identifier) {
64    // Accept SMS input formats with 1- or 2-digit country code, 
65    // 3-digit area code, and 7-digit number.
66    // You can customize the SMS regex to allow different formats.
67    String smsRegex = '^(\\+?\\d{1,2}?[\\s-])?(\\(?\\d{3}\\)?[\\s-]?\\d{3}[\\s-]?\\d{4})$';
68    Pattern smsPattern = Pattern.compile(smsRegex);
69    Matcher smsMatcher = SmsPattern.matcher(identifier);
70    if (smsMatcher.matches()) {
71        try {
72            // Format user input into the verified SMS format '+xx xxxxxxxxxx' 
73            // before DB lookup. If no country code is provided, append 
74            // US country code +1 for the default.
75            String countryCode = smsMatcher.group(1) == null ? '+1' : smsMatcher.group(1);
76            return System.UserManagement.formatPhoneNumber(countryCode, smsMatcher.group(2));
77        } catch(System.InvalidParameterValueException e) {
78            return null;
79        }
80    } else { return null; }
81}
82private PageReference getSsoRedirect(User user, String startUrl, Map<String, String> requestAttributes) {
83    // You can look up to check whether the user should log in with 
84    // SAML or an Auth Provider and return the URL to initialize SSO.
85    return null;
86}
87private PageReference discoveryResult(User user, Auth.VerificationMethod method, String startUrl, Map<String, String> requestAttributes) {
88    // Only external users with an External Identity or community license can log in 
89    // using Site.passwordlessLogin. Use getSsoRedirect to let internal users 
90    // log in to a community.
91    PageReference ssoRedirect = getSsoRedirect(user, startUrl, requestAttributes);
92    if (ssoRedirect != null) {
93        return ssoRedirect;
94    } else {
95        if (method != null) {
96            List<Auth.VerificationMethod> methods = new List<Auth.VerificationMethod>();
97            methods.add(method);
98            PageReference pwdlessRedirect = Site.passwordlessLogin(user.Id, methods, startUrl);
99            if (pwdlessRedirect != null) {
100                return pwdlessRedirect;
101            } else {
102                throw new Auth.LoginDiscoveryException('No Passwordless Login redirect URL returned for verification method: ' + method);
103            }
104        } else {
105            throw new Auth.LoginDiscoveryException('No method found');
106        }
107    }
108}
109}

コード例: プロファイルによるログイン検出のユーザの絞り込み

本番組織では、同じ検証済みメールアドレスと携帯電話番号を持つ複数のユーザが存在することが可能です。ただし、顧客は一意のユーザしか持てません。この問題に対処するには、固有性を確保するために数行のコードを追加し、ユーザをプロファイルで絞り込めるようにします。このコード例では、外部 ID ユーザプロファイルを持つユーザが処理されますが、他のユースケースをサポートするように調整できます。たとえば、他のユーザライセンスまたは基準を持つユーザに対応するようにコードの最初の行を変更できます。

Login Discovery は、Customer Community、Customer Community Plus、External Identity、Partner Community、Partner Community Plus などのすべての外部ユーザライセンスで使用可能です。この変更の内容は、どのプロファイルがコミュニティにアクセスできるかによって異なります。

1global class AutocreatedDiscLoginHandler1551301979709 implements Auth.LoginDiscoveryHandler {
2
3global PageReference login(String identifier, String startUrl, Map<String, String> requestAttributes) {
4    if (identifier != null && isValidEmail(identifier)) {
5        // Ensure uniqueness by profile
6        Profile p = [SELECT id FROM profile WHERE name = 'External Identity User'];
7        List<User> users = [SELECT Id FROM User WHERE Email = :identifier AND IsActive = TRUE AND profileId=:p.id];
8        if (!users.isEmpty() && users.size() == 1) {
9            // User must have verified email before using this verification method. We cannot send messages to unverified emails. 
10            // You can check if the user has email verified bit on and add the password verification method as fallback.
11            List<TwoFactorMethodsInfo> verifiedInfo = [SELECT HasUserVerifiedEmailAddress FROM TwoFactorMethodsInfo WHERE UserId = :users[0].Id];
12            if (!verifiedInfo.isEmpty() && verifiedInfo[0].HasUserVerifiedEmailAddress == true) {
13                // Use email verification method if the user's email is verified.
14                return discoveryResult(users[0], Auth.VerificationMethod.EMAIL, startUrl, requestAttributes);
15            } else {
16                // Use password verification method as fallback if the user's email is unverified.
17                return discoveryResult(users[0], Auth.VerificationMethod.PASSWORD, startUrl, requestAttributes);
18            }
19        } else {
20            throw new Auth.LoginDiscoveryException('No unique user found. User count=' + users.size());
21        }
22    }
23    if (identifier != null) {
24        String formattedSms = getFormattedSms(identifier);
25        if (formattedSms != null) {
26            // Ensure uniqueness by profile
27            Profile p = [SELECT id FROM profile WHERE name = 'External Identity User'];
28            List<User> users = [SELECT Id FROM User WHERE MobilePhone = :formattedSms AND IsActive = TRUE AND profileId=:p.id];
29            if (!users.isEmpty() && users.size() == 1) {
30                // User must have verified SMS before using this verification method. We cannot send messages to unverified mobile numbers. 
31                // You can check if the user has mobile verified bit on or add the password verification method as fallback.
32                List<TwoFactorMethodsInfo> verifiedInfo = [SELECT HasUserVerifiedMobileNumber FROM TwoFactorMethodsInfo WHERE UserId = :users[0].Id];
33                if (!verifiedInfo.isEmpty() && verifiedInfo[0].HasUserVerifiedMobileNumber == true) {
34                    // Use SMS verification method if the user's mobile number is verified.
35                    return discoveryResult(users[0], Auth.VerificationMethod.SMS, startUrl, requestAttributes);
36                } else {
37                    // Use password verification method as fallback if the user's mobile number is unverified.
38                    return discoveryResult(users[0], Auth.VerificationMethod.PASSWORD, startUrl, requestAttributes);
39                }
40            } else {
41                throw new Auth.LoginDiscoveryException('No unique user found. User count=' + users.size());
42            }
43        }
44    }
45    if (identifier != null) {
46        // You can customize the code to find user via other attributes, such as SSN or Federation ID
47    }
48    throw new Auth.LoginDiscoveryException('Invalid Identifier');
49}
50
51private boolean isValidEmail(String identifier) {
52    String emailRegex = '^[a-zA-Z0-9._|\\\\%#~`=?&/$^*!}{+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}$';
53    // source: http://www.regular-expressions.info/email.html 
54    Pattern EmailPattern = Pattern.compile(emailRegex);
55    Matcher EmailMatcher = EmailPattern.matcher(identifier);
56    if (EmailMatcher.matches()) { return true; }
57    else { return false; }
58}
59
60private String getFormattedSms(String identifier) {
61    // Accept SMS input formats with 1 or 2 digits country code, 3 digits area code and 7 digits number
62    // You can customize the SMS regex to allow different formats
63    String smsRegex = '^(\\+?\\d{1,2}?[\\s-])?(\\(?\\d{3}\\)?[\\s-]?\\d{3}[\\s-]?\\d{4})$';
64    Pattern smsPattern = Pattern.compile(smsRegex);
65    Matcher smsMatcher = SmsPattern.matcher(identifier);
66    if (smsMatcher.matches()) {
67        try {
68            // Format user input into the verified SMS format '+xx xxxxxxxxxx' before DB lookup
69            // Append US country code +1 by default if no country code is provided
70            String countryCode = smsMatcher.group(1) == null ? '+1' : smsMatcher.group(1);
71            return System.UserManagement.formatPhoneNumber(countryCode, smsMatcher.group(2));
72        } catch(System.InvalidParameterValueException e) {
73            return null;
74        }
75    } else { return null; }
76}
77
78private PageReference getSsoRedirect(User user, String startUrl, Map<String, String> requestAttributes) {
79    // You can look up if the user should log in with SAML or an Auth Provider and return the URL to initialize SSO.
80    return null;
81}
82
83private PageReference discoveryResult(User user, Auth.VerificationMethod method, String startUrl, Map<String, String> requestAttributes) {
84    //Only external users with an External Identity or community license can login using Site.passwordlessLogin
85    //Use getSsoRedirect to enable internal user login for a community
86    PageReference ssoRedirect = getSsoRedirect(user, startUrl, requestAttributes);
87    if (ssoRedirect != null) {
88        return ssoRedirect;
89    } else {
90        if (method != null) {
91            List<Auth.VerificationMethod> methods = new List<Auth.VerificationMethod>();
92            methods.add(method);
93            PageReference pwdlessRedirect = Site.passwordlessLogin(user.Id, methods, startUrl);
94            if (pwdlessRedirect != null) {
95                return pwdlessRedirect;
96            } else {
97                throw new Auth.LoginDiscoveryException('No Passwordless Login redirect URL returned for verification method: ' + method);
98            }
99        } else {
100            throw new Auth.LoginDiscoveryException('No method found');
101        }
102    }
103}
104}