BLOG ARTICLE

Passkey Authentication for Salesforce Digital Experience (Community Cloud) – Part 1

Share This Post

Throughout this article, we will guide you on how to implement Passkey Authentication for Salesforce Digital Experience (Community Cloud). This blog article is divided into 2 parts. In the first part, we will cover the Registration, and in the second part, we will address the Authentication. Stay tuned for more!

What is Passkey Authentication?

Simply put, passkeys are unique digital keys that replace traditional passwords, eliminating the need to memorize complex combinations of letters, numbers, and symbols. These cryptographically secure keys are stored locally on your device, shielding them from the prying eyes of hackers and eliminating the risk of data breaches.

Unlike passwords, passkeys are immune to phishing scams and social engineering tactics, as they are never transmitted over the internet. Instead, they rely on a secure communication protocol between your device and the website or app you’re accessing. This ensures that only you, the rightful owner of the passkey, can unlock your digital kingdom.

Technology behind Passkey Authentication

Passkey authentication is based on WebAuthn and public key cryptography. It uses a variety of factors, such as a device’s fingerprint or face unlock, to authenticate users.

Public key cryptography is a type of cryptography that uses two keys: a public key and a private key. The public key is used to encrypt data, and the private key is used to decrypt it. The public and private keys are mathematically related, but it is almost impossible to calculate the private key from the public key.

Passkey authentication works by generating a public/private key pair on the user’s device. The public key is shared with the website or app that the user is registering with, and the private key is stored securely on the user’s device.

When the user wants to log in to the website or app, they are sent a challenge. The user’s device uses the private key to sign the challenge and send it back to the website or app. The website or app verifies the signed challenge and logs the user in.

https://www.passkeys.io/technical-details has a very good technical write up and visual flows on how passkey authentication happens.

Salesforce Setup

Unfortunately at the moment Salesforce supports WebAuthn authentication only as a second factor, so users won’t be able to login with it. But hey! We have all the power of the platform, why don’t we try to add it ourselves?

We’ll use LWR Community Template and LWC components to trigger browser JavaScript API navigator.credentials methods and Apex to store and validate keys and digital signatures.

Full flow Demo that we going to implement:

Registration

NOTE:

Passkey registration can be triggered for both Authenticated and Unauthenticated users, for the purpose of our demo we’ll consider flow for users that are already registered and add Passkey as an alternative authentication method.

Pre-Registration Steps

To trigger registration we need to provide minimum following details to navigator.credentials method:

  • Community Details: Host and Name
  • User Details: User Id, User Name, Display Name
  • Challenge – randomly generated string that will be signed by User’s device

We’ll get Community Details from Apex Controller which will return us Site.getMasterLabel() as Name and new URL(Site.getBaseSecureUrl()).getHost() as Host:

				
					public static final URL COMMUNITY_URL {
    public get {
        if(COMMUNITY_URL == null) {
            if (!Test.isRunningTest()) {
                COMMUNITY_URL = new URL(Site.getBaseSecureUrl());
            } else {
                COMMUNITY_URL = new URL('https://fidopasskey-sample.my.salesforce-sites.com');
            }
        }
        return COMMUNITY_URL;
    }
    private set;
}

@AuraEnabled(cacheable=true)
public static ExperienceInfo getExperienceInfo() {
    return new ExperienceInfo(Site.getMasterLabel(), COMMUNITY_URL.getHost());
}

public with sharing class ExperienceInfo {
    @AuraEnabled
    public String name;
    @AuraEnabled
    public String host;

    public ExperienceInfo(String name, String host) {
        this.name = name;
        this.host = host;
    }
}

				
			

To get User Details, we’ll use standard lightning/uiRecordApi:

				
					import { LightningElement, wire } from 'lwc';
import userId from "@salesforce/user/Id";
import { getRecord } from "lightning/uiRecordApi";
import getExperienceInfo from "@salesforce/apex/PasskeyRegistrationController.getExperienceInfo";

const FIELDS = ["User.Name", "User.Email"];


export default class passkeyRegistration extends LightningElement {

    passkeyAvailable = false;
    showRegistrationSuccess = false;

    get passkeyDisabled() {
        return !this.passkeyAvailable;
    }

    @wire(getRecord, { recordId: userId, fields: FIELDS })
    user;

    @wire(getExperienceInfo)
    experienceInfo;
}

				
			

We’ll generate challenge within Apex and store it in user session cache, so that we could validate it after passkey generation:

				
					private static final String CHALLENGE_KEY = 'local.passkey.challenge';

@AuraEnabled
public static String generateChallenge() {
    String challenge = Utils.getRandomString(32);
    Cache.Session.put(CHALLENGE_KEY, challenge);
    return challenge;
}


				
			

Before we trigger registration requests we need to verify that the browser and device user is currently using does actually support Passkey Authentication. To do that we’ll use following JavaScript function: 

				
					const checkPasskeyAvailability = () => {
    // Availability of `window.PublicKeyCredential` means WebAuthn is usable.  
    // `isUserVerifyingPlatformAuthenticatorAvailable` means the feature detection is usable.  
    // `​​isConditionalMediationAvailable` means the feature detection is usable.  
    console.log('Attempting to load window.PublicKeyCredential');
    if (window.PublicKeyCredential &&  
        window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&  
        window.PublicKeyCredential.isConditionalMediationAvailable) {  
            // Check if user verifying platform authenticator is available.  
            return Promise.all([  
                window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),  
                window.PublicKeyCredential.isConditionalMediationAvailable(),  
            ]).then(results => { 
                if (results.every(r => r === true)) {  
                    console.log('window.PublickKeyCredential is active');
                    return true;
                }
                return false;
            }).catch(error => {
                console.log('Failed to load window.PublicKeyCredential: ' + error);
                return false;
            });
    }  else {
        console.log('window.PublicKeyCredential is not available');
        return false;
    }
}


				
			

It will return us a Promise that will resolve into True or False depending on the feature availability. It is best to trigger this function within connectedCallback() of our component, so that we could hide or show the Registration button.

Triggering Registration

Now that we know Passkey is supported by device and we have all required details to start registration, lets trigger navigator.credentials.create():

				
					let publicKeyCredentialCreationOptions = {
    challenge: string2ArrayBuffer(challenge),
    rp: {
        id: this.experienceInfo.data.host,
        name: this.experienceInfo.data.name
    },
    user: {
        id: string2ArrayBuffer(userId),
        name: this.user.data.fields.Email.value,
        displayName: this.user.data.fields.Name.value,
    },
    pubKeyCredParams: [
        { alg: -7, type: "public-key" }, // "ES256" as registered in the IANA COSE Algorithms registry
        { alg: -257, type: "public-key" }, // Value registered by this specification for "RS256"
    ],
    authenticatorSelection: {
        authenticatorAttachment: "platform",
        residentKey: "required",
        requireResidentKey: "true",
        userVerification: "required",
    },
};

navigator.credentials.create({  
    publicKey: publicKeyCredentialCreationOptions  
}).then(credential => {

    let registrationInfo = {
        credentialId: credential.id,
        rawId: arrayBuffer2Base64Url(credential.rawId),
        type: credential.type,
        publicKey: arrayBuffer2Base64(credential.response.getPublicKey()),
        publicKeyAlgorithm: credential.response.getPublicKeyAlgorithm(),
        clientDataJSON: new TextDecoder().decode(credential.response.clientDataJSON)
    };
}).catch(function (err) {
    console.log(err);
    // No acceptable auth device or user refused consent. Handle appropriately.
});

				
			

This will trigger a Passkey generation process on the user’s device. It will include biometrics verification, depending on locally supported type, it could be either fingerprints or face verification. 

As you can see below my Mac triggered TouchId verification:

As a result of registration our LWC will receive a AuthenticatorAttestationResponse object, which includes:

  • Credential Id – unique identifier of generated passkey
  • Public Key – public key that can be used to verify subsequent authentication requests
  • Public Key Algorithm – algorithm which was used during public key generation
  • clientDataJSON – client data object
                      ° challenge – challenge that was used during registration
                      ° origin – base URL of the page where registration was triggered
                      ° type – fixed to ‘webauthn.create’
 

NOTE:

AuthenticatorAttestationResponse also returns an attestationObject which is used as part of the attestation process. This process is optional, as per WebAuthn standard, and primarily used as part of Enterprise authentication flows where it is required to verify that an Authenticator device is allowed as part of this flow. In most B2C cases this is not needed and we won’t cover it in the Demo.

Post-Registration Steps

Passkey was created on the user’s device, now we need to pass it to Salesforce, so that we could use it during authentication.

To store passkey details we’ll create a custom object User Credential related to standard User object:

WARNING:

While the Public Key is not a secret and one can not simply use it to generate signed authentication requests, it is still best to store it encrypted. For the purpose of our demo, I will use an unencrypted text field.

Before we save data into our new Custom Object, we’ll need to perform some validations:

  • type is ‘public-key’
  • publicKeyAlgorithm is either -7 (ECDSA-SHA256) or -256 (HmacSHA256)
  • publicKey is not null
  • clientDataJSON.type is webauthn.create
  • clientDataJSON.challenge is same as the one stored in Session cache
  • clientDataJSON.origin is same as community host

If all validations do pass, we save passkey and clear Cache value:

				
					public static Boolean registerPasskey(RegistrationInfo regInfo) {
    try {
        //Validate basic information

        // Ensure ID is base64url-encoded
        if (regInfo.credentialId != regInfo.rawId || regInfo.credentialId == null) {
            throw new PasskeyRegistrationException('Credential ID was not base64url-encoded');
        }

        // Make sure credential type is public-key
        if (regInfo.type != 'public-key') {
            throw new PasskeyRegistrationException('Unexpected credential type ${credentialType}, expected "public-key"');
        }

        ClientData cData = (ClientData) JSON.deserialize(regInfo.clientDataJSON, ClientData.class);

        if(cData.type != 'webauthn.create') {
            throw new PasskeyRegistrationException('Unexpected registration response type: ' + cData.type);
        }

        String algorithm = ALGORITHMS.get(regInfo.publicKeyAlgorithm);
        if(algorithm == null) {
            throw new PasskeyRegistrationException('Unexpected public key algoritn: ' + regInfo.publicKeyAlgorithm);
        }

        if(String.isBlank(regInfo.publicKey)) {
            throw new PasskeyRegistrationException('No Public Key found');
        }

        // Ensure the device provided the challenge we gave it
        String expectedChallenge = (String) Cache.Session.get('local.passkey.challenge');
        if (cData.challenge == null || Utils.base64UrlDecode(cData.challenge) != expectedChallenge) {
            throw new PasskeyRegistrationException(
                'Unexpected registration response challenge "' + cData.challenge + '", expected "' + expectedChallenge + '"'
            );
        }

        // Check that the origin is our site
        URL originUrl = new URL(Site.getBaseSecureUrl());
        String expectedOrigin = originUrl.getProtocol() + '://' + originUrl.getHost();
        if(cData.origin != expectedOrigin) {
            throw new PasskeyRegistrationException(
                'Unexpected registration response origin "' + cData.origin + '", expected "' + expectedOrigin +'"'
            );
        }

        Blob publicKeyBlob = EncodingUtil.base64Decode(regInfo.publicKey);

        insert as system new User_Credential__c(User__c = UserInfo.getUserId(),
                                                Credential_Id__c = regInfo.credentialId,
                                                Public_Key__c = regInfo.publicKey,
                                                Public_Key_Algorithm__c = algorithm);

        //Remove challenge from cache
        Cache.Session.remove(CHALLENGE_KEY);

        return true;
    } catch (Exception ex) {
        System.debug(ex.getMessage() + '\n' + ex.getStackTraceString());
        throw new AuraHandledException('Failed to register Passkey');
    }
}


				
			

We are all set now! We have credentials saved and ready for users to authenticate which will be covered in the next section.

About the Author
Nazim Aliyev, CTO @ Nubessom

Nazim Aliyev, CTO @ Nubessom

Salesforce Consultant & Architect, Business Applications, Business Processes and Technology Consulting, Entrepreneurship. More than 15 years working on the CRM and Salesforce Ecosystems.

Let´s talk about your challenge!

    In order to provide you the content requested, we need to store and process your personal data. If you consent to us storing your personal data for this purpose, please tick the checkbox below.


    You can unsubscribe from these communications at any time. For more information on how to unsubscribe, our privacy practices, and how we are committed to protecting and respecting your privacy, please review our Privacy Policy.

    Need more Inspiration? keep reading Our related content

    Blog Article

    Working with Time data type in Flows

    Discover innovative solutions to manage time data types in Salesforce Flow, including overcoming time zone challenges and the absence of native Time data support. Learn how to employ Apex classes and custom LWC components to ensure accurate time inputs in local time zones, enhancing data precision and user interaction in your Salesforce applications. 
    Read More »