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
- User clicks "Confirm transfer"
- Backend creates a challenge for the user's factor
- User enters the code in your UI
- Backend sends the code to the verify endpoint
- 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.
| Namespace | Examples | Description |
|---|---|---|
reauth | reauth | Generic "confirm it's you" before a sensitive page |
account.* | account.delete, account.email_change | Account lifecycle actions |
password.* | password.change, password.reset | Password operations |
billing.* | billing.payment_method, billing.subscription | Financial and billing operations |
transaction.* | transaction.approve, transaction.withdraw | Transaction confirmation |
admin.* | admin.role_change, admin.config_change | Administrative actions |
Purpose is validated against the pattern: /^[a-z][a-z0-9_.]{2,50}$/
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