Protecting sensitive actions with identity verification
Require identity verification (2FA, MFA, biometrics) before users can access or modify sensitive financial data like bank accounts and SSN/TIN.
If your application already has an identity verification system (2FA, MFA, biometrics, or similar), you can integrate it with Salsa's embedded UI to protect sensitive actions. This allows you to bring your own verification flow while using Salsa's payroll components.
When to use this
Use this feature when:
- You want to require step-up authentication before users can view or edit sensitive data
- You have an existing MFA/2FA provider and want to use it with Salsa's embedded UI
- Your compliance requirements mandate additional verification for financial operations
How it works
When a user with a token of basic role attempts a protected action, Salsa displays a verification prompt to start verification process. Your application handles the actual verification and upgrading token once user verified their identity. Example of how the flow may look for adding a bank account:
sequenceDiagram
participant User
participant SalsaUI as Salsa UI
participant YourApp as Your App
participant IDProvider as Identify Provider
participant SalsaAPI as Salsa API
User->>SalsaUI: Opens protected action (e.g. "Add Bank Account")
SalsaUI->>User: Shows MFA/2FA prompt
User->>YourApp: Start verification
SalsaUI->>YourApp: request-privileged-access event
YourApp->>User: Show verification flow
User->>YourApp: Verify identity
YourApp->>IDProvider: Verify identity
IDProvider->>YourApp: Verification success
YourApp->>SalsaAPI: Create admin token
SalsaAPI->>YourApp: Return admin token
YourApp->>SalsaUI: replaceUserToken(adminToken)
SalsaUI->>User: Proceeds with protected action
Protected actions
The following actions are protected and require admin token:
- Adding bank accounts (both employer and worker)
- Viewing or editing worker SSN/TIN
Note that these actions may be accessed through several workflows of the embedded UI experiences (e.g. onboarding, bank accounts).
Responsibilities
You are responsible for:
- Verification method: Implementing and managing your 2FA, MFA, biometrics, or other identity verification
- Token lifecycle: Creating, upgrading, and downgrading user tokens
- TTL and role: Defining how long privileged access lasts and what role it grants
Salsa's responsibilities are limited to:
- Triggering the
request-privileged-accessevent when accessing a protected action - Accepting the token(s) you provide
- Displaying the verification prompt UI when user has insufficient privilege
Edge cases and error handling
Handle these scenarios in your implementation:
| Scenario | Suggested handling |
|---|---|
| User abandons flow | Call element.cancelRequestForPrivilegedAccess() to close prompt and return user to previous view. |
| User fails verification | Provide means for user to retry, or call element.cancelRequestForPrivilegedAccess() to close prompt and return user to previous view. |
| Admin token expires | Make sure to replace with valid token before token TTL expires. If not, user will see a typical permissions error. |
| Token is downgraded | User will be prompted to verify identity again to continue with the protected action. |
Implementation steps
Prerequisites
Before implementing this feature, ensure you have:
- Familiarity with Salsa authorization and token roles
- An existing identity verification system (2FA/MFA provider, biometrics, etc.)
- Ability to create user tokens via the Credentials API
Step 1: Create a basic role token
Create a user token with a basic role. This restricts access to sensitive actions until verification is complete. See Salsa authorization for how.
Step 2: Configure the element
Enable the verification workflow when creating the Salsa element:
const element = salsa.elements.create("employer-bank-accounts", {
userToken: basicToken, // Token with "basic" role
allowRequestForPrivilegedAccess: true, // Enables verification workflow
});Step 3: Handle the request event
Listen for the request-privileged-access event and implement your verification flow (pseudo code):
element.on("request-privileged-access", async (event) => {
// Show your MFA/2FA UI and wait for user to complete verification
const verificationResult = await showYourVerificationFlow();
if (verificationResult.success) {
// Create an admin token
const adminToken = await createAdminToken();
// Upgrade the token - this closes the prompt and allows user to proceed
element.replaceUserToken(adminToken);
} else {
// Edge cases and error handling
if (verificationResult.cancelled) {
element.cancelRequestForPrivilegedAccess();
} else {
showErrorMessage("Verification failed. Please try again.");
retryYourVerificationFlow();
}
}
});Step 4: Manage token expiration
Before the admin token expires, either downgrade to a basic token or refresh with a new admin token:
setTimeout(async () => {
const basicToken = await createBasicToken();
element.replaceUserToken(basicToken);
// Alt: Refresh with new admin token
const newAdminToken = await createAdminToken();
element.replaceUserToken(newAdminToken);
}, timeout); // before TTL of tokenComplete end-to-end example
Here's a full implementation showing the entire flow (pseudo code):
// State management
let tokenRefreshTimer = null;
// Create element with basic token
async function initializeElement() {
const basicToken = await createToken("EMPLOYER_BASIC");
const element = salsa.elements.create("employer-bank-accounts", {
userToken: basicToken,
allowRequestForPrivilegedAccess: true,
});
// Handle verification requests
element.on("request-privileged-access", handleVerificationRequest);
element.mount("#bank-accounts-container");
return element;
}
// Verification flow
async function handleVerificationRequest(event) {
try {
// 1. Show your verification UI
const result = await showMFAModal();
if (result.cancelled) {
// Closes prompt in Salsa UI and returns user to previous view
element.cancelRequestForPrivilegedAccess();
return;
}
if (!result.success) {
showToast("Verification failed or cancelled");
// TODO Allow user to retry, or call element.cancelRequestForPrivilegedAccess() to abort
return;
}
// 2. Get privileged token from your backend
const { token, expiresAt } = await createToken("EMPLOYER_ADMIN");
// 3. Upgrade the element's token
element.replaceUserToken(token);
// 4. Schedule downgrade before expiration
scheduleTokenDowngrade(element, expiresAt);
} catch (error) {
element.cancelRequestForPrivilegedAccess();
showToast("Verification failed. Please try again.");
console.error("Verification error:", error);
}
}
// Token lifecycle management
function scheduleTokenDowngrade(element, expiresAt) {
// Clear any existing timer
if (tokenRefreshTimer) {
clearTimeout(tokenRefreshTimer);
}
// Downgrade before expiry, e.g. 30s prior
const msUntilExpiry = new Date(expiresAt) - Date.now();
const downgradeIn = msUntilExpiry - 30_000;
tokenRefreshTimer = setTimeout(async () => {
const basicToken = await createToken("EMPLOYER_BASIC");
element.replaceUserToken(basicToken);
showToast("Session returned to standard access");
}, downgradeIn);
}
// Backend token creation (pseudocode)
async function createToken(role) {
const response = await fetch("/api/salsa/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
role: role,
ttlMinutes: role === "EMPLOYER_ADMIN" ? 5 : 60,
}),
});
return response.json();
}
// Initialize on page load
const element = await initializeElement();Short-lived privileges
For security, it's advised to create admin tokens with a short TTL (recommended: 5-15 minutes). This limits exposure if the token is compromised and aligns with security best practices for privileged access.
Without verification enabled
If you create a basic role token without enabling allowRequestForPrivilegedAccess, users are simply blocked from protected actions:
This may be useful when you want to restrict access entirely rather than allow step-up verification. For example, for a user that may have permissions to edit some information but should never have privilege to view/edit the protected actions.
Updated 24 days ago
