Please check your email for a sign-up confirmation containing a one time pass code.
Please check your email for a one time pass code to complete sign-in.
Please enter your email below, and we will send a one time passcode to your email.
Please check your email for a one time pass code.
Please enter and confirm your new password below:
Open the Javascript console, once open press the button that corrisponds with what you are looking to do.
Let's setup Salesforce's new Headless Authenticaiton. To start with, please go to sign up for a new Developer Org here. Once you have access to your new org, and you have signed in and verified your email, you may continue to the next section.
If you are a part of the Guest Session Beta or have the ability to enable the "Authentication: Enable using code credential guest flow" org perm in Black Tab, and would like to test guest session enable the checkbox below.
In this section we will guide you through the basic setup required for your Saleforce Org. It is assumed you are logged in as an admin user, and have full access to setup. It is highly recommended to only perform this setup in a non-production development enviorment.
To create a role go to Setup -> Users -> Roles and create a new role.
To assign the Role to your admin user, go to Setup -> Users -> Users and select your admin user. Click edit and assign the role to your user using the role drop down.
To enable the Auth Code and Cred flow, go to Setup -> Identity -> OAuth and OpenID Connect Settings and enable the flow for the org.
Clone the Customer Community User profile. To do this go to Setup -> Profiles, select Customer Community User, and clone it. Name it "Headless Demo Profile".
To enable CORS go to Setup -> CORS and create a new entry for: https://headless-identity.herokuapp.com.
External Users need an account to be a parent for their contact records. Create an Account called "My Account", this Account will be referenced by the Auth Provider and Apex Registration Handlers provided later in the setup guide.
In this section we will setup a Google Auth Provider so that you can see how the new sso_provider param works for webserver code flows, and it how it can be used to create native feeling login forms.
To do this go to Setup -> Auth Providers and click new. Select Google from the drop down list.
Set the name of your Auth Provider to "Google IDP", set the URL Suffix to "GOOGLE_IDP".
Download the demo apex auth provider registration handler here. After reviewing the registration handler, upload it to your org or copy and paste it into a new Apex Class in the Developer Console. Once uploaded into the org, assign the Demo Apex Auth Provider Registration handler to your Auth Provider. Specify you admin user as the Run-As user. Finally save your auth provider.
Warning: Never upload code to your org without reviewing it and ensuring that it does what you expect.
The headless Registration API and Forgot Password API can be setup to be protected by Google reCaptcha to support public clients. To do this go to the Google reCaptcah admin console and add a new site. Call it Headless Demo, choose recaptcha v3, and enter "headless-identity.herokuapp.com" as the domain. Once done, copy and paste your site key below. Please also take note of the site secret as you will need it for the next section.
External Users and their APIs are ultimately exposed through a digital experience. So that means even for a headless experience we must still setup a Digital Experience Site.
To do this go to Setup -> Digital Experiences and enable Digital Experiences. Once enabled click "New", choose the "Build Your Own (LWR)" template, name the site "Headless Demo". If possible do not proivde a path post fix. If you do it is fine but you must remember to include it when asked for the experience cloud domain. Copy the final URL into the Experience Cloud Domain field below, and click "Create".
From the Experience Cloud Workspace, click the "Administration" tile. On the left hand side, select "Members". Add the "Headless Demo Profile to the Selected Profile pane, and click "Save".
From the Experience Cloud Workspace, click the "Administration" tile. On the left hand side, select "Login & Registration".
Under "Login Page" enable "Google IDP"
Under "Headless Identity Configuration" enable "Allow Self Registration via the Headless Registration API". Once you have done that, a few other checkboxes will appear. Enable "Require reCAPTCHA to access this API" and disable "Require authentication to access this API".
Select the "Headless Demo Profile" for the Default Profile.
Download the Demo Headless Registration Handler here, after reviewing it, upload it to your org, or copy and paste it into the developer console. Assign it as the Registration Handler, and set the Run-As user to your admin user.
Under "Headless Forgot Password Enable "Allow password reset via the Headless Forgot Password API". Once enabled a few other options will appear. Enable "Require reCAPTCHA to access this API" and Disable "Require authentication to access this API". Keep the attempts at the default 5.
reCAPTCHA Options for Headless Identity copy and past the reCaptcha Site Secret Key into the "Secret Key" field. Set the Score Threshold to .5.
Click Save, as you have now completed the setup of this section.
From the Experience Cloud Workspace, click the "Administration" tile. On the left hand side, select "Settings", and click the "Activate" button
To enable headless Identity you need to create a OAuth enabled Connected App in your org. This section guides you through that setup.
To do this go to Setup -> Apps -> App Manager and click "New Connected App" in the top right corner. Fill in the basic information for the Connected App, and name the App "Headless Identity Demo".
Enable OAuth for the Connected App by checking the "Enable OAuth Settings".
Add the following two scopes to your app: "Manage user data via APIs (api)" and "Access unique user identifiers (openid)".
Disable "Require Secret for Web Server Flow" and "Require Secret for Refresh Token Flow" as this application is a public client and cannot keep secrets secure.
Enable the Headless Flow by checking the box next to "Enable Authorization Code and Credentials Flow"
Click Save, and then Continue
On the Connected App Detail Page, click "Manage Consumer Details". Complete the 2fa prompt, and copy the consumer key. Paste it below.
From the Connectd App Detail Page, click the "Manage" button, and then click "Edit Policies".
Under "OAuth Policies" set the "Permitted User" setting to "Admin approved users are pre-authorized".
Under "Authorization Code and Credentials Flow", enable "Enable for Guest Users".
Click Save
Scroll down to the Connected App Profile Related List, cick "Manage Profiles" and select the "Headless Demo Profile" and click Save.
You have completed your Org Setup, please click finish below
Click the downlaod button below to download a Postman collection that has been configured with the information you have entered during this tutorial.
The Postman collection consists of user registration, user login, forgot password, user information, and token revocation. To help make it easier to use the postman collection, this web app contains utilities for generatating things like recaptcha tokens. You can find these in the nav bar of this application by pressing the "Utilities" Button.
Now that you have setup your Salesforce org, we want to talk to you about how you can interact with the APIs via Javascript, just like this app does. In the below sections we will go over each flow, and provide a high level example of the javascript function used to achieve the outcome.
The core flow that our headless APIs use is called the Authorization Code and Credential flow. This is a modified version of the OAuth Auth Code Flow (Webserver Code Flow). The core idea is that you make a authorization request to the /authorize endpoint. You pass the username and password base 64 encoded in the header.
// Make a POST Request to Authorize client = new XMLHttpRequest(); client.open("POST", expDomain + authorizationURI, true); client.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); //Headers for the Username and Password Code and Cred Flow client.setRequestHeader("Auth-Request-Type", "Named-User"); client.setRequestHeader("Authorization", "Basic " + btoa(username + ':' + password)); //Request Body requestBody = "response_type=code_credentials&client_id=" + clientId + "&redirect_uri=" + callbackURL; // Add State if (state != null) { requestBody = requestBody + '&state=' + storeState(state); } // Add Scopes, Guest Flows require a Scope if (scopes != null) { requestBody = requestBody + '&scope=' + scopes; } // PKCE Enabled requestBody = requestBody + "&code_challenge=" + generateCodeChallenge(); // Send the Authorization Request client.send(requestBody); // Handle the Authorization Response client.onreadystatechange = function() { if(this.readyState == 4) { if (this.status == 200) { //Auth Code has been returned, perform token exchange tokenExchange(JSON.parse(client.response), null, authorizeType, uniqueVisitorId); } else { onError("An Error Occured during Authorize", client.response); } } }
Salesforce then validates the username and password, and if they are correct returns a Auth Code response. From there the app, uses the code to perform a code exchange at the token endpoint to get the access token.
function tokenExchange(response, codeChallenge, authorizeType, uniqueVisitorId) { // Get Values from Code Response code = response.code; stateIdentifier = response.state; baseURL = response.sfdc_community_url; state = null; // validate state if it was present if (stateIdentifier != null) { state = getState(stateIdentifier, true); if (state == null) { onError("A state param was sent back but no state was found"); return; } } // Create Client client = new XMLHttpRequest(); client.open("POST", expDomain + tokenURI, true); client.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); // Build Request Body requestBody = "code=" + code + "&grant_type=authorization_code&client_id=" + _this.clientId + "&redirect_uri=" + _this.callbackURL; // Add PKCE requestBody = requestBody + "&code_verifier=" + generateCodeVerifier(); // Send Request client.send(requestBody); client.onreadystatechange = function() { if(this.readyState == 4) { if (this.status == 200) { //Access Tokens have been returned console.log("Code and Credntial Flow, token response: "); console.log(JSON.parse(client.response)); } else { onError("An error occured during token exchange for " + authorizeType, client.response) } } } }
The end result of the above javascript code is a access token (and possibly a refresh token), you can now request user information and establish a session within your application.
Registration is built ontop of the core Auth Code and Credential flow and has pre-req step. The flow works by first submitting your user registration data to a new endpoint called "/services/auth/headless/init/registration". Once this registration details are submitted, the end point validates the authentication header or the reCAPTCHA token, stores the data and returns a request identifier. An OTP is then sent to customers email address or SMS number.
function registerNewUser(registrationData, verificationMethod, callbackFunction) { client = new XMLHttpRequest(); client.open("POST", expDomain + initRegistrationURI, true); client.setRequestHeader("Content-Type", "application/json"); client.setRequestHeader("Auth-Verification-Type",verificationMethod); //This is a JSON structure that contains userData, customData, recaptcha, password client.send(JSON.stringify(registrationData)); client.onreadystatechange = function() { if(this.readyState == 4) { if (this.status == 200) { //If this call is successful, we get back a request identifier. callbackFunction(JSON.parse(client.response), verificationMethod) } else { console.log(client.response) onError("An Error Occured during init registration call", client.response); } } } }
The rest of the Registraton flow is nearly identical to the Username Password Auth Code and Cred flow. Instead of passing the username/password in the header, you instead pass the request identifier and OTP generated from the previous call. The only other major difference is the inclusion of additional headers which can be seen in the code sample below.
// Make a POST Request to Authorize client = new XMLHttpRequest(); client.open("POST", expDomain + authorizationURI, true); client.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); //Headers for registration variation of the Code and Credentials flow client.setRequestHeader("Auth-Request-Type", "user-registration"); client.setRequestHeader("Auth-Verification-Type", verificationMethod); //Request identifier is returned from the /init/registration endpoint, and requestCredential is the OTP sent in the email or SMS. client.setRequestHeader("Authorization", "Basic " + btoa(requestIdentifier + ':' + requestCredential)); //Request Body requestBody = "response_type=code_credentials&client_id=" + clientId + "&redirect_uri=" + callbackURL; // Add State if (state != null) { requestBody = requestBody + '&state=' + storeState(state); } // Add Scopes, Guest Flows require a Scope if (scopes != null) { requestBody = requestBody + '&scope=' + scopes; } // PKCE Enabled requestBody = requestBody + "&code_challenge=" + generateCodeChallenge(); // Send the Authorization Request client.send(requestBody); // Handle the Authorization Response client.onreadystatechange = function() { if(this.readyState == 4) { if (this.status == 200) { //Auth Code has been returned, perform token exchange tokenExchange(JSON.parse(client.response), null, authorizeType, uniqueVisitorId); } else { onError("An Error Occured during Authorize", client.response); } } }
Salesforce then validates the request identifier and OTP, registers the user using the Headless Registration Handler using the data submitted in the /init/registration call. Once that is complete the Auth Code response is returned. From there the app, uses the code to perform a code exchange at the token endpoint to get the access token. The code exhcange portion of the flow is identitical to the Username Password Code Exchange
function tokenExchange(response, codeChallenge, authorizeType, uniqueVisitorId) { // Get Values from Code Response code = response.code; stateIdentifier = response.state; baseURL = response.sfdc_community_url; state = null; // validate state if it was present if (stateIdentifier != null) { state = getState(stateIdentifier, true); if (state == null) { onError("A state param was sent back but no state was found"); return; } } // Create Client client = new XMLHttpRequest(); client.open("POST", expDomain + tokenURI, true); client.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); // Build Request Body requestBody = "code=" + code + "&grant_type=authorization_code&client_id=" + _this.clientId + "&redirect_uri=" + _this.callbackURL; // Add PKCE requestBody = requestBody + "&code_verifier=" + generateCodeVerifier(); // Send Request client.send(requestBody); client.onreadystatechange = function() { if(this.readyState == 4) { if (this.status == 200) { //Access Tokens have been returned console.log("Code and Credntial Flow, token response: "); console.log(JSON.parse(client.response)); } else { onError("An error occured during token exchange for " + authorizeType, client.response) } } } }
The end result of the above javascript code is a access token (and possibly a refresh token), you can now request user information and establish a session within your application.
Whether you plan to change a password for an existing user a user forgets their password, the new headless forgot password API provides you a self service path to resolution. The API consists of a single endpoint, "/services/auth/headless/forgot_password". The process itself consists of two calls to the same endpoint, one to initialize the request, the other to complete the request and change the password.
The first call, submits a username and a valid reCAPTCAHA token to the endpoint. The response from this endpoint is a confirmation that an OTP has been sent. This is always the response so that we do not leak information. The OTP is sent ot the end users email address.
//This is the function call to initialize the forgot password request forgotPasswordRequest(username, null, null, recapchaToken, forgotPasswordProcess.init, callbackFunction);
The second request is made containing the username, new password, and OTP that was sent to the end user during the first request.
//This is the function call to complete the forgot password request forgotPasswordRequest(username, password, otp, null, forgotPasswordProcess.changePassword, callbackFunction)
As the API endpoint is the same for each call, and the only thing that changes is the params passed to the call, the core function below can be shared accross both calls.
function forgotPasswordRequest(username, password, otp, recapchaToken, forgotPasswordProcessStep, callbackFunction) { client = new XMLHttpRequest(); client.open("POST", expDomain + forgotPasswordURI, true); client.setRequestHeader("Content-Type", "application/json"); requestBody = { username: username, newpassword: password, otp: otp, recaptcha: recapchaToken } client.send(JSON.stringify(requestBody)); client.onreadystatechange = function() { if(this.readyState == 4) { if (this.status == 200) { callbackFunction(JSON.parse(client.response), username, forgotPasswordProcessStep) } else { this.onError("An Error Occured during Forgot Password Step: " + forgotPasswordProcessStep, client.response); } } } }
Our new sso_provider parameter allows you to use a redirect based flow but make it appear that your application nativily integrates with the IDP instead of with Salesforce (which then forwards you to the IDP). To use this flow we construct an authorization URL to Saleforce but specify the Auth Provider URL Suffix in the sso_provider param.
//Setup the Authorization URL redirectURL = expDomain + _authorizationURI; //Add Params to the Authorization URL redirectURL = redirectURL + '?client_id=' + _this.clientId; redirectURL = redirectURL + '&redirect_uri=' + _this.ssoCallbackURL; redirectURL = redirectURL + '&state=' + storeState(state); redirectURL = redirectURL + '&response_type=code'; //Specificy the SSO Provider redirectURL = redirectURL + '&sso_provider=' + ssoProviderDevName; //Add Scopes if (scopes!= null) { redirectURL = redirectURL + '&scopes=' + scopes; } //Add Code Challenge requestBody = requestBody + "&code_challenge=" + generateCodeChallenge(); //Redirect the Browser window.location.href = redirectURL;
From here the flow behaves like any other redirect based flow. The user will be redirected to your experience cloud side, and then automatically redirected to the SSO Provider. Once they sign-in they will be redirected back to Salesfroce, and then automatically redirected back to our app. Once the user makes it back to our app, we have to handle the redirect and perform the code exchange. The code below does exactly that.
// Get URL Params from the callback URL queryString = window.location.search; urlParams = new URLSearchParams(queryString); console.log('Loading Callback Params: ' + urlParams); //Create the Code Response from the URL params codeResponse = new Object; codeResponse.code = urlParams.get('code'); codeResponse.state = urlParams.get('state'); codeResponse.sfdc_community_url = urlParams.get('sfdc_community_url'); // Call the common token exhcange method. tokenExchange(codeResponse, getCodeChallenge(), authorizationType.SSOLogin, null);
Once the callback URL data is marshalled together, the common token exchange code can be called below:
function tokenExchange(response, codeChallenge, authorizeType, uniqueVisitorId) { // Get Values from Code Response code = response.code; stateIdentifier = response.state; baseURL = response.sfdc_community_url; state = null; // validate state if it was present if (stateIdentifier != null) { state = getState(stateIdentifier, true); if (state == null) { onError("A state param was sent back but no state was found"); return; } } // Create Client client = new XMLHttpRequest(); client.open("POST", expDomain + tokenURI, true); client.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); // Build Request Body requestBody = "code=" + code + "&grant_type=authorization_code&client_id=" + _this.clientId + "&redirect_uri=" + _this.callbackURL; // Add PKCE requestBody = requestBody + "&code_verifier=" + generateCodeVerifier(); // Send Request client.send(requestBody); client.onreadystatechange = function() { if(this.readyState == 4) { if (this.status == 200) { //Access Tokens have been returned console.log("Code and Credntial Flow, token response: "); console.log(JSON.parse(client.response)); } else { onError("An error occured during token exchange for " + authorizeType, client.response) } } } }
Once you have an access token you retrieve user information by making a request to the user info endpoint located here: "/services/oauth2/userinfo". This request is authenticated with an Authorization Bearer header as shown below.
function getUserInfo(accessToken) { client = new XMLHttpRequest(); client.open("GET", expDomain + userInfoURI, true); client.setRequestHeader("Content-Type", "application/json"); client.setRequestHeader("Authorization", 'Bearer ' + accessToken); client.send(); client.onreadystatechange = function() { if(this.readyState == 4) { if (this.status == 200) { //User Info response console.log(client.response); } else { console.log(client.response) onError("An Error Occured during Forgot Password Step: " + forgotPasswordProcessStep, client.response); } } } }