Newer Version Available
HeadlessUserDiscoveryHandler Interface
Namespace
Usage
With headless passwordless login, you can build a flow where a user logs in to a headless, off-platform app by entering their email address, phone number, or another identifier that you choose. After you collect the user's identifier, your app passes it to the headless passwordless login endpoint in a login_hint parameter. At this point, you can use a headless user discovery handler to find the user account associated with the data that you passed in the login_hint. With a handler, you can give users more ways to log in and have more control over your headless passwordless login implementation.
For more information, see these resources.
HeadlessUserDiscoveryHandler Methods
The following are methods for HeadlessUserDiscoveryHandler.
discoverUserFromLoginHint(networkId, loginHint, verificationAction, customDataJson, requestAttributes)
Signature
public Auth.HeadlessUserDiscoveryResponse discoverUserFromLoginHint(Id networkId, String loginHint, Auth.VerificationAction verificationAction, String customDataJson, Map<String,String> requestAttributes)
Parameters
- networkId
- Type: Id
- The ID of the Experience Cloud site where your headless app sends requests.
- loginHint
- Type: String
- Information about the user that Salesforce can use to find their associated account, such as their email address or phone number.
- verificationAction
- Type: Auth.VerificationAction
- The verification method that's used to log the user in, either email or SMS.
- customDataJson
- Type: String
- Custom user data, such as first name, that you collect when the user logs in to your headless app.
- requestAtttibutes
- Type: Map<String,String>
- Information about the login request that's 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: Auth.HeadlessUserDiscoveryResponse
If the handler finds a user, it returns a user ID. If not, it returns an error message.
HeadlessUserDiscoveryHandler Example Implementation
Here's an example implementation of the Auth.HeadlessUserDiscoveryHandler interface. This example supports login with email and login with SMS.
The discoverUserFromLoginHint method uses custom logic to search for a user account with a verified email address or phone number that matches the data passed in the login hint. As a security best practice, Salesforce always recommends writing code to determine if the user's email address or phone number is verified.
For users logging in with email, the custom logic first checks whether the email address passed in the login hint is in a valid format. Then, to look for a verified Salesforce email address that matches the email address passed in the login hint, it queries the TwoFactorMethodsInfo object. If successful, it returns an instance of Auth.HeadlessUserDiscoveryResponse with the user ID. If something goes wrong, it returns an instance of Auth.HeadlessUserDiscoveryResponse with a custom error message. In this example, it returns error messages when the email address format isn't valid, the email address isn't verified, there's no user with that email address, or there are multiple users with that email address.
For users logging in with SMS, the custom logic is similar. It checks whether the phone number passed in the login hint is in a valid format. Then, it looks for a verified Salesforce phone number that matches the phone number passed in the login hint. If successful, it returns an instance of Auth.HeadlessUserDiscoveryResponse with the user ID, and if not, it returns custom error messages.
1/*
2 * Headless User Discovery Handler
3 */
4global class MyHeadlessUserDiscoveryHandler implements Auth.HeadlessUserDiscoveryHandler {
5
6
7 /*
8 * This method handles the logic to determine the user account based on the loginHint and verificationMethod
9 */
10 global Auth.HeadlessUserDiscoveryResponse discoverUserFromLoginHint(Id networkId, String loginHint,
11 Auth.VerificationAction verificationAction, String customDataJson, Map<String,String>requestAttributes) {
12 if (verificationAction == Auth.VerificationAction.EMAIL) {
13 return doLookupByVerifiedEmail(loginHint, verificationAction);
14 } else if (verificationAction == Auth.VerificationAction.SMS) {
15 return doLookupByVerifiedMobile(loginHint, verificationAction);
16 } else {
17 return new Auth.HeadlessUserDiscoveryResponse(null, 'Unsupported Auth.VerificationAction');
18 }
19 }
20
21 private Auth.HeadlessUserDiscoveryResponse doLookupByVerifiedEmail(String loginHint, Auth.VerificationAction verificationAction) {
22 if (String.isBlank(loginHint) || !isValidEmail(loginHint)) {
23 return new Auth.HeadlessUserDiscoveryResponse(null, 'Invalid email sent as loginHint: ' + loginHint);
24 }
25 // Search for an user account by email
26 List<User> users = [SELECT Id FROM User WHERE Email = :loginHint AND IsActive = TRUE];
27 if (!users.isEmpty() && users.size() == 1) {
28 Id userId = users[0].Id;
29 // Check if the user has a verified email
30 List<TwoFactorMethodsInfo> verifiedInfo = [SELECT HasUserVerifiedEmailAddress FROM TwoFactorMethodsInfo WHERE UserId = :userId];
31 if (!verifiedInfo.isEmpty() && verifiedInfo[0].HasUserVerifiedEmailAddress == true) {
32 // Prepare and return HeadlessUserDiscoveryResponse with userId
33 return new Auth.HeadlessUserDiscoveryResponse(new Set<Id>{userId}, null);
34 } else {
35 // Return HeadlessUserDiscoveryResponse with error message
36 return new Auth.HeadlessUserDiscoveryResponse(null, 'Email ' + loginHint + ' not verified for the given user account');
37 }
38 } else {
39 if (users.isEmpty()) {
40 return new Auth.HeadlessUserDiscoveryResponse(null, 'No user identified for the email: ' + loginHint);
41 } else {
42 return new Auth.HeadlessUserDiscoveryResponse(null, 'Multiple users identified for the email: ' + loginHint);
43 }
44 }
45 }
46
47 private Auth.HeadlessUserDiscoveryResponse doLookupByVerifiedMobile(String loginHint, Auth.VerificationAction verificationAction) {
48 String formattedSms = !String.isBlank(loginHint) ? getFormattedSms(loginHint) : null;
49 if (String.isBlank(formattedSms)) {
50 return new Auth.HeadlessUserDiscoveryResponse(null, 'Invalid phone number sent as loginHint: ' + loginHint);
51 }
52 // Search for an user account by phone
53 List<User> users = [SELECT Id FROM User WHERE MobilePhone = :loginHint AND IsActive = TRUE];
54 if (!users.isEmpty() && users.size() == 1) {
55 Id userId = users[0].Id;
56 // Check if the user has a verified phone
57 List<TwoFactorMethodsInfo> verifiedInfo = [SELECT HasUserVerifiedMobileNumber FROM TwoFactorMethodsInfo WHERE UserId = :userId];
58 if (!verifiedInfo.isEmpty() && verifiedInfo[0].HasUserVerifiedMobileNumber == true) {
59 // Prepare and return HeadlessUserDiscoveryResponse with userId
60 return new Auth.HeadlessUserDiscoveryResponse(new Set<Id>{userId}, null);
61 } else {
62 // Return HeadlessUserDiscoveryResponse with error message
63 return new Auth.HeadlessUserDiscoveryResponse(null, ' ' + loginHint + ' not verified for the given user account');
64 }
65 } else {
66 if (users.isEmpty()) {
67 return new Auth.HeadlessUserDiscoveryResponse(null, 'No user identified for the phone number: ' + loginHint);
68 } else {
69 return new Auth.HeadlessUserDiscoveryResponse(null, 'Multiple users identified for the phone number: ' + loginHint);
70 }
71 }
72 }
73
74 private boolean isValidEmail(String identifier) {
75 String emailRegex = '^[a-zA-Z0-9._|\\\\%#~`=?&/$^*!}{+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}$';
76 // source: http://www.regular-expressions.info/email.html
77 Pattern EmailPattern = Pattern.compile(emailRegex);
78 Matcher EmailMatcher = EmailPattern.matcher(identifier);
79 if (EmailMatcher.matches()) { return true; }
80 else { return false; }
81 }
82
83 private String getFormattedSms(String identifier) {
84 // Accept SMS input formats with 1 or 2 digits country code, 3 digits area code and 7 digits number
85 // You can customize the SMS regex to allow different formats
86 String smsRegex = '^(\\+?\\d{1,2}?[\\s-])?(\\(?\\d{3}\\)?[\\s-]?\\d{3}[\\s-]?\\d{4})$';
87 Pattern smsPattern = Pattern.compile(smsRegex);
88 Matcher smsMatcher = SmsPattern.matcher(identifier);
89 if (smsMatcher.matches()) {
90 try {
91 // Format user input into the verified SMS format '+xx xxxxxxxxxx' before DB lookup
92 // Append US country code +1 by default if no country code is provided
93 String countryCode = smsMatcher.group(1) == null ? '+1' : smsMatcher.group(1);
94 return System.UserManagement.formatPhoneNumber(countryCode, smsMatcher.group(2));
95 } catch(System.InvalidParameterValueException e) {
96 return null;
97 }
98 } else { return null; }
99 }