Skip to main content

Step-Up MFA Verification

Use VoxKey's MFA Verification API to confirm sensitive actions -- like money transfers, password changes, or admin operations -- by challenging users with their enrolled MFA factors. Your application delegates the entire MFA flow to VoxKey: no need to implement TOTP validation, rate limiting, or brute-force protection yourself.

Why step-up MFA?

MFA at login confirms who the user is. Step-up MFA confirms the user is still present when it matters most -- deleting an account, transferring money, changing a password, or creating an API key.

Without step-up verification, a compromised session (stolen token, unattended laptop) can perform any action the user is authorized for.

Quick start: TOTP verification

The simplest integration requires three API calls: list factors, create a challenge, then verify the code.

const REALM = 'your-realm-uuid';
const BASE = `https://app.voxkey.io/api/v1/${REALM}/mfa`;

// 1. List the user's enrolled factors
const factors = await fetch(`${BASE}/factors`, {
headers: { Authorization: `Bearer ${accessToken}` },
}).then(r => r.json());

const totp = factors.enrolled.find(f => f.type === 'totp');
if (!totp) throw new Error('User has no TOTP factor enrolled');

// 2. Create a challenge
const challenge = await fetch(`${BASE}/challenges`, {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
factor_type: 'totp',
purpose: 'transaction.approve',
}),
}).then(r => r.json());

// 3. Verify the user's code
const result = await fetch(`${BASE}/challenges/${challenge.challenge_id}/verify`, {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ code: userEnteredCode }),
}).then(r => r.json());

if (result.verified) {
// Proceed with the sensitive action
// result.step_up_token is available if issue_step_up_token was set
}

Integration scenarios

Traditional Web (server-side)

Your backend calls VoxKey directly. The verification result is trusted because it never leaves the server.

Browser --> Your Backend --> VoxKey MFA API (challenge + verify) --> verified: true --> execute action
  1. User clicks "Confirm transfer"
  2. Backend creates a challenge for the user's factor
  3. User enters the code in your UI
  4. Backend sends the code to the verify endpoint
  5. If verified === true, execute the action immediately

No step-up token is needed -- the server already holds the trusted result.

Single-Page Application (SPA)

The SPA calls VoxKey (through your backend proxy, or directly if the access token is scoped). After verification, the API returns a step-up token -- a short-lived signed JWT that your backend validates before executing the action.

Browser --> VoxKey MFA API (challenge + verify) --> step_up_token
Browser --> Your Backend (action + X-Step-Up-Token header)
Your Backend --> validate JWT via JWKS --> execute action

Request the token by setting issue_step_up_token: true when creating the challenge. See Step-Up Token for validation details.

Microservices

Service A verifies the user and passes the step_up_token to Service B which performs the action. Service B validates the token independently via JWKS -- no callback to VoxKey required.

SPA --> API Gateway --> VoxKey MFA API --> step_up_token
--> API Gateway --> Payment Service (validate token via JWKS) --> execute

The token is self-contained (signed JWT), so downstream services do not need to call VoxKey again to verify it.

Purpose conventions

The purpose field is a required string that binds the challenge to a specific action. Use dot-separated namespaces to keep purposes organized.

NamespaceExamplesDescription
reauthreauthGeneric "confirm it's you" before a sensitive page
account.*account.delete, account.email_changeAccount lifecycle actions
password.*password.change, password.resetPassword operations
billing.*billing.payment_method, billing.subscriptionFinancial and billing operations
transaction.*transaction.approve, transaction.withdrawTransaction confirmation
admin.*admin.role_change, admin.config_changeAdministrative actions

Purpose is validated against the pattern: /^[a-z][a-z0-9_.]{2,50}$/

tip

A step-up proof for account.delete cannot be reused for billing.subscription. Your backend should always check that the token's purpose claim matches the action being performed.

What happens on failure

  • Wrong code: The challenge's attempt counter increments. After 5 failed attempts, the challenge is locked and returns 410 Gone.
  • Expired challenge: Challenges have a TTL (5 minutes for TOTP/WebAuthn, 10 minutes for email/SMS). After expiration, create a new challenge.
  • Rate limit: More than 5 challenge creations per minute per user returns 429 Too Many Requests. Back off and retry.

Next steps

  • MFA API Reference -- full endpoint reference with all parameters and responses
  • Step-Up Token -- token structure and validation for SPA and microservices
  • MFA Factor Types -- comparison of supported factors with rate limits and TTLs