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 External Identity Implementation Guide.
Here’s the method for LoginDiscoveryHandler.
public System.PageReference login(String identifier, String startUrl, Map<String,String>requestAttributes)
Here’s a sample requestAttributes response.
CommunityUrl=http://my-developer-edition.mycompany.com:5555/discover
IpAddress=55.555.0.0
UserAgent=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
Platform=Mac OSX
Application=Browser
City=San Mateo
Country=United States
Subdivision=California
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 community 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.
global class AutocreatedDiscLoginHandler1535377170343 implements Auth.LoginDiscoveryHandler { global PageReference login(String identifier, String startUrl, Map<String, String> requestAttributes) { if (identifier != null && isValidEmail(identifier)) { // Search for user by email. List<User> users = [SELECT Id FROM User WHERE Email = :identifier AND IsActive = TRUE]; if (!users.isEmpty() && users.size() == 1) { // User must have a verified email before using this verification method. // We cannot send messages to unverified emails. // You can check if the user's email verified bit set and add the // password verification method as fallback. List<TwoFactorMethodsInfo> verifiedInfo = [SELECT HasUserVerifiedEmailAddress FROM TwoFactorMethodsInfo WHERE UserId = :users[0].Id]; if (!verifiedInfo.isEmpty() && verifiedInfo[0].HasUserVerifiedEmailAddress == true) { // Use email verification method if the user's email is verified. return discoveryResult(users[0], Auth.VerificationMethod.EMAIL, startUrl, requestAttributes); } else { // Use password verification method as fallback // if the user's email is unverified. return discoveryResult(users[0], Auth.VerificationMethod.PASSWORD, startUrl, requestAttributes); } } else { throw new Auth.LoginDiscoveryException('No unique user found. User count=' + users.size()); } } if (identifier != null) { String formattedSms = getFormattedSms(identifier); if (formattedSms != null) { // Search for user by SMS. List<User> users = [SELECT Id FROM User WHERE MobilePhone = :formattedSms AND IsActive = TRUE]; if (!users.isEmpty() && users.size() == 1) { // User must have a verified SMS before using this verification method. // We cannot send messages to unverified mobile numbers. // You can check if the user's mobile verified bit is set or add // the password verification method as fallback. List<TwoFactorMethodsInfo> verifiedInfo = [SELECT HasUserVerifiedMobileNumber FROM TwoFactorMethodsInfo WHERE UserId = :users[0].Id]; if (!verifiedInfo.isEmpty() && verifiedInfo[0].HasUserVerifiedMobileNumber == true) { // Use SMS verification method if the user's mobile number is verified. return discoveryResult(users[0], Auth.VerificationMethod.SMS, startUrl, requestAttributes); } else { // Use password verification method as fallback if the user's // mobile number is unverified. return discoveryResult(users[0], Auth.VerificationMethod.PASSWORD, startUrl, requestAttributes); } } else { throw new Auth.LoginDiscoveryException('No unique user found. User count=' + users.size()); } } } if (identifier != null) { // You can customize the code to find user via other attributes, // such as SSN or Federation ID. } throw new Auth.LoginDiscoveryException('Invalid Identifier'); } private boolean isValidEmail(String identifier) { String emailRegex = '^[a-zA-Z0-9._|\\\\%#~`=?&/$^*!}{+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}$'; // source: http://www.regular-expressions.info/email.html Pattern EmailPattern = Pattern.compile(emailRegex); Matcher EmailMatcher = EmailPattern.matcher(identifier); if (EmailMatcher.matches()) { return true; } else { return false; } } private String getFormattedSms(String identifier) { // Accept SMS input formats with 1- or 2-digit country code, // 3-digit area code, and 7-digit number. // You can customize the SMS regex to allow different formats. String smsRegex = '^(\\+?\\d{1,2}?[\\s-])?(\\(?\\d{3}\\)?[\\s-]?\\d{3}[\\s-]?\\d{4})$'; Pattern smsPattern = Pattern.compile(smsRegex); Matcher smsMatcher = SmsPattern.matcher(identifier); if (smsMatcher.matches()) { try { // Format user input into the verified SMS format '+xx xxxxxxxxxx' // before DB lookup. If no country code is provided, append // US country code +1 for the default. String countryCode = smsMatcher.group(1) == null ? '+1' : smsMatcher.group(1); return System.UserManagement.formatPhoneNumber(countryCode, smsMatcher.group(2)); } catch(System.InvalidParameterValueException e) { return null; } } else { return null; } } private PageReference getSsoRedirect(User user, String startUrl, Map<String, String> requestAttributes) { // You can look up to check whether the user should log in with // SAML or an Auth Provider and return the URL to initialize SSO. return null; } private PageReference discoveryResult(User user, Auth.VerificationMethod method, String startUrl, Map<String, String> requestAttributes) { // Only external users with an External Identity or community license can log in // using Site.passwordlessLogin. Use getSsoRedirect to let internal users // log in to a community. PageReference ssoRedirect = getSsoRedirect(user, startUrl, requestAttributes); if (ssoRedirect != null) { return ssoRedirect; } else { if (method != null) { List<Auth.VerificationMethod> methods = new List<Auth.VerificationMethod>(); methods.add(method); PageReference pwdlessRedirect = Site.passwordlessLogin(user.Id, methods, startUrl); if (pwdlessRedirect != null) { return pwdlessRedirect; } else { throw new Auth.LoginDiscoveryException('No Passwordless Login redirect URL returned for verification method: ' + method); } } else { throw new Auth.LoginDiscoveryException('No method found'); } } } }
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 to all external user licenses, including Customer Community, Customer Community Plus, External Identity, Partner Community, and Partner Community Plus. It depends on which profiles have access to your community.
global class AutocreatedDiscLoginHandler1551301979709 implements Auth.LoginDiscoveryHandler { global PageReference login(String identifier, String startUrl, Map<String, String> requestAttributes) { if (identifier != null && isValidEmail(identifier)) { // Ensure uniqueness by profile Profile p = [SELECT id FROM profile WHERE name = 'External Identity User']; List<User> users = [SELECT Id FROM User WHERE Email = :identifier AND IsActive = TRUE AND profileId=:p.id]; if (!users.isEmpty() && users.size() == 1) { // User must have verified email before using this verification method. We cannot send messages to unverified emails. // You can check if the user has email verified bit on and add the password verification method as fallback. List<TwoFactorMethodsInfo> verifiedInfo = [SELECT HasUserVerifiedEmailAddress FROM TwoFactorMethodsInfo WHERE UserId = :users[0].Id]; if (!verifiedInfo.isEmpty() && verifiedInfo[0].HasUserVerifiedEmailAddress == true) { // Use email verification method if the user's email is verified. return discoveryResult(users[0], Auth.VerificationMethod.EMAIL, startUrl, requestAttributes); } else { // Use password verification method as fallback if the user's email is unverified. return discoveryResult(users[0], Auth.VerificationMethod.PASSWORD, startUrl, requestAttributes); } } else { throw new Auth.LoginDiscoveryException('No unique user found. User count=' + users.size()); } } if (identifier != null) { String formattedSms = getFormattedSms(identifier); if (formattedSms != null) { // Ensure uniqueness by profile Profile p = [SELECT id FROM profile WHERE name = 'External Identity User']; List<User> users = [SELECT Id FROM User WHERE MobilePhone = :formattedSms AND IsActive = TRUE AND profileId=:p.id]; if (!users.isEmpty() && users.size() == 1) { // User must have verified SMS before using this verification method. We cannot send messages to unverified mobile numbers. // You can check if the user has mobile verified bit on or add the password verification method as fallback. List<TwoFactorMethodsInfo> verifiedInfo = [SELECT HasUserVerifiedMobileNumber FROM TwoFactorMethodsInfo WHERE UserId = :users[0].Id]; if (!verifiedInfo.isEmpty() && verifiedInfo[0].HasUserVerifiedMobileNumber == true) { // Use SMS verification method if the user's mobile number is verified. return discoveryResult(users[0], Auth.VerificationMethod.SMS, startUrl, requestAttributes); } else { // Use password verification method as fallback if the user's mobile number is unverified. return discoveryResult(users[0], Auth.VerificationMethod.PASSWORD, startUrl, requestAttributes); } } else { throw new Auth.LoginDiscoveryException('No unique user found. User count=' + users.size()); } } } if (identifier != null) { // You can customize the code to find user via other attributes, such as SSN or Federation ID } throw new Auth.LoginDiscoveryException('Invalid Identifier'); } private boolean isValidEmail(String identifier) { String emailRegex = '^[a-zA-Z0-9._|\\\\%#~`=?&/$^*!}{+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}$'; // source: http://www.regular-expressions.info/email.html Pattern EmailPattern = Pattern.compile(emailRegex); Matcher EmailMatcher = EmailPattern.matcher(identifier); if (EmailMatcher.matches()) { return true; } else { return false; } } private String getFormattedSms(String identifier) { // Accept SMS input formats with 1 or 2 digits country code, 3 digits area code and 7 digits number // You can customize the SMS regex to allow different formats String smsRegex = '^(\\+?\\d{1,2}?[\\s-])?(\\(?\\d{3}\\)?[\\s-]?\\d{3}[\\s-]?\\d{4})$'; Pattern smsPattern = Pattern.compile(smsRegex); Matcher smsMatcher = SmsPattern.matcher(identifier); if (smsMatcher.matches()) { try { // Format user input into the verified SMS format '+xx xxxxxxxxxx' before DB lookup // Append US country code +1 by default if no country code is provided String countryCode = smsMatcher.group(1) == null ? '+1' : smsMatcher.group(1); return System.UserManagement.formatPhoneNumber(countryCode, smsMatcher.group(2)); } catch(System.InvalidParameterValueException e) { return null; } } else { return null; } } private PageReference getSsoRedirect(User user, String startUrl, Map<String, String> requestAttributes) { // You can look up if the user should log in with SAML or an Auth Provider and return the URL to initialize SSO. return null; } private PageReference discoveryResult(User user, Auth.VerificationMethod method, String startUrl, Map<String, String> requestAttributes) { //Only external users with an External Identity or community license can login using Site.passwordlessLogin //Use getSsoRedirect to enable internal user login for a community PageReference ssoRedirect = getSsoRedirect(user, startUrl, requestAttributes); if (ssoRedirect != null) { return ssoRedirect; } else { if (method != null) { List<Auth.VerificationMethod> methods = new List<Auth.VerificationMethod>(); methods.add(method); PageReference pwdlessRedirect = Site.passwordlessLogin(user.Id, methods, startUrl); if (pwdlessRedirect != null) { return pwdlessRedirect; } else { throw new Auth.LoginDiscoveryException('No Passwordless Login redirect URL returned for verification method: ' + method); } } else { throw new Auth.LoginDiscoveryException('No method found'); } } } }