Newer Version Available
LoginDiscoveryHandler Interface
Namespace
Usage
Implement a Auth.LoginDiscoveryHandler for an interview-based log in. The handler looks up a user from the identifier entered, and can call Site.passwordlessLogin to determine which credential to use, such as email or SMS. Or the handler can redirect a user to a third-party identity provider for login. With this handler, the login page doesn't show a password field. However, you can use Site.passwordlessLogin to then prompt for a password.
From the user perspective, the user enters an identifier at the log in prompt. Then the user completes the login by entering a PIN or password. Or, if SSO-enabled, the user bypasses login.
For an example, see LoginDiscoveryHandler Example Implementation. For more details, see Salesforce Customer Identity in Salesforce Help.
LoginDiscoveryHandler Method
Here’s the method for LoginDiscoveryHandler.
login(identifier, startUrl, requestAttributes)
Signature
public System.PageReference login(String identifier, String startUrl, Map<String,String>requestAttributes)
Parameters
- identifier
- Type: String
- Identifier the customer or partner entered at the login prompt, for example, an email address or phone number.
- startUrl
- Type: String
- Path to the Experience Cloud site page requested by the customer or partner. The user is redirected to this location after successful login.
- requestAttributes
- Type: Map<String,String>
- Information about the login request based on the user’s browser state when accessing the login page. requestAttributes passes in the CommunityUrl, IpAddress, UserAgent, Platform, Application, City, Country, and Subdivision values. The City, Country, and Subdivision values come from IP geolocation.
Example
Here’s a sample requestAttributes response.
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 Example Implementation
This Apex code example implements the Auth.LoginDiscoveryHandler interface. It checks whether the user who is logging in has a verified email or phone number, depending on which identifier was supplied on the login page. If verified, with Auth.VerificationMethod.EMAIL or Auth.VerificationMethod.SMS, we send a challenge to the identifier, either the user’s email address or mobile device. If the user enters the code correctly on the verify page, the user is redirected to the Experience Cloud site’s page specified by the start URL. If the user isn’t verified, the user must enter a password to log in. The handler also checks that the email and phone number are unique with this code: users.size()==1.
The discoveryResult function calls the Site.passwordlessLogin method to log the user in with the specified verification method. The getSsoRedirect function looks up whether the user logs in with SAML or an Auth Provider. Add the implementation-specific logic to handle the lookup.
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}Code Example: Filter Login Discovery Users by Profile
Your production org can have multiple users with the same verified email address and mobile number. But your customers must have unique ones. To address this problem, you can add a few lines of code that filters users by profile to ensure uniqueness. This code example handles users with the External Identity User profile, but can be adapted to support other use cases. For example, you can modify the first line of code to address users with other user licenses or criteria.
Login Discovery is available with the following user licenses: Customer Community, Customer Community Plus, External Identity, Partner Community, and Partner Community Plus. It depends on which profiles have access to your Experience Cloud site.
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}