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-access event 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:

ScenarioSuggested handling
User abandons flowCall element.cancelRequestForPrivilegedAccess() to close prompt and return user to previous view.
User fails verificationProvide means for user to retry, or call element.cancelRequestForPrivilegedAccess() to close prompt and return user to previous view.
Admin token expiresMake sure to replace with valid token before token TTL expires. If not, user will see a typical permissions error.
Token is downgradedUser 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 token

Complete 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.