LoginDiscoveryHandler インターフェース
名前空間
使用方法
インタビューベースのログイン用の Auth.LoginDiscoveryHandler を実装します。このハンドラは、入力された ID からユーザを検索し、Site.passwordlessLogin をコールして、使用するログイン情報 (メール、SMS など) を決定できます。または、ハンドラはユーザをログイン用のサードパーティ ID プロバイダにリダイレクトできます。このハンドラを使用した場合、ログインページにパスワード項目は表示されません。ただし、Site.passwordlessLogin を使用して、パスワードを要求できます。
ユーザの観点では、ユーザはログイン画面で ID を入力します。次に、PIN またはパスワードを入力してログインを完了します。または、SSO が有効になっている場合、ユーザはログインをスキップします。
例については、「LoginDiscoveryHandler の実装例」を参照してください。詳細は、Salesforce ヘルプの「Salesforce Customer Identity」を参照し���ください。
LoginDiscoveryHandler メソッド
LoginDiscoveryHandler のメソッドは次のようになります。
login(identifier, startUrl, requestAttributes)
署名
public System.PageReference login(String identifier, String startUrl, Map<String,String>requestAttributes)
パラメータ
- identifier
- 型: String
- ログイン画面で顧客またはパートナーが入力した識別子 (メールアドレスや電話番号など)。
- startUrl
- 型: String
- 顧客またはパートナーが要求した Experience Cloud サイトページへのパス。ログインが成功すると、ユーザはこの場所にリダイレクトされます。
- requestAttributes
- 型: Map<String,String>
- ログインページにアクセスしたときのユーザのブラウザ状態に基づくログイン要求に関する情報。requestAttributes は、CommunityUrl、IpAddress、UserAgent、Platform、Application、City、Country、Subdivision の値を渡します。City、Country、Subdivision の値は IP 地理位置情報から取得されます。
例
次に 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=CaliforniaLoginDiscoveryHandler の実装例
この Apex コード例は Auth.LoginDiscoveryHandler インターフェースを実装します。ログインページで提供された ID に応じて、ログインしているユーザのメールまたは電話番号が検証済みかどうかをチェックします。検証済みの場合 (Auth.VerificationMethod.EMAIL または Auth.VerificationMethod.SMS)、ID (ユーザのメールアドレスまたはモバイルデバイス) にチャレンジを送信します。検証ページでユーザがコードを正しく入力すると、ユーザは開始 URL で指定された Experience Cloud サイトのページにリダイレクトされます。ユーザが検証されない場合、ユーザはログインのためのパスワードを入力する必要があります。また、ハンドラは users.size()==1 のコードによってメールアドレスと電話番号が一意であることも確認します。
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: https://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 users with an External Identity or community license can log in
89 // using Site.passwordlessLogin. Use getSsoRedirect to let your org employees
90 // log in to an Experience Cloud site.
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 ユーザライセンスで使用可能です。これは、どのプロファイルが Experience Cloud サイトにアクセスできるかによって異なります。
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: https://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 users with an External Identity or community license can login using Site.passwordlessLogin
85 //Use getSsoRedirect to enable your org employees to log in to an Experience Cloud site
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}