Newer Version Available

This content describes an older version of this product. View Latest

LoginDiscoveryHandler Interface

Salesforce gives you the ability to log in users based on other verification methods than username and password. For example, it can prompt users to log in with their email, phone number, or another identifier like a Federation ID or device identifier. Login Discovery is available to these licenses: Customer Community, Customer Community Plus, External Identity, Partner Community, and Partner Community Plus.

Namespace

Auth

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)

Log in the customer or partner given the specified identifier, such as email or phone number. If successful, redirect the user to the Experience Cloud site page specified by the start URL.

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.

Return Value

Type: System.PageReference

The URL of the page where the user is redirected.

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=California

LoginDiscoveryHandler 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.

Passwordless login works only with verified methods. You can check the verification status on the User object, for example, with User list view, a report, or the API. Make sure that your solution handles the case where the user doesn’t have a verification method. This code example falls back to a password.

The default discoverable login handler checks whether the user entered a valid email address or phone number before redirecting the user to the verification page. If an invalid entry is made, the handler returns an error. Because this behavior is vulnerable to user enumeration attack, make sure that your solution prevents this attack. For example, you can create a dummy page similar to the verification page and redirect the user to the dummy page when invalid user identifier is entered. Also, use generic error messages to avoid providing additional information.

Note

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}