Step-Up Token
Step-up токен -- опциональный подписанный JWT, возвращаемый после успешной MFA верификации. Он предоставляет криптографическое доказательство того, что пользователь прошёл step-up верификацию для конкретного действия.
Когда использовать
| Архитектура | Нужен step-up токен? | Почему |
|---|---|---|
| Серверное приложение (бэкенд вызывает VoxKey напрямую) | Нет | Ваш бэкенд уже получил verified: true -- он доверяет ответу VoxKey |
| SPA (фронтенд вызывает VoxKey) | Да | Ваш бэкенд не может доверять фронтенду утверждающему "MFA пройден" -- токен предоставляет доказательство |
| Микросервисы (Сервис A верифицирует, Сервис B действует) | Да | Сервис B валидирует токен независимо без обращения к VoxKey |
Запрос step-up токена
Установите issue_step_up_token: true при создании challenge:
const challenge = await fetch('/api/v1/{realm}/mfa/challenges', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
factor_type: 'totp',
purpose: 'account.delete',
issue_step_up_token: true,
}),
}).then(r => r.json());
После успешной верификации ответ включает step_up_token:
{
"verified": true,
"factor_type": "totp",
"verified_at": "2026-04-03T10:04:30Z",
"step_up_token": "eyJhbGciOiJSUzI1NiIs..."
}
Передача токена
Отправляйте его в заголовке X-Step-Up-Token при вызове вашего бэкенда:
await fetch('/api/delete-account', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'X-Step-Up-Token': stepUpToken,
},
});
Справочник claims
| Claim | Тип | Описание |
|---|---|---|
typ | string | Всегда step_up+jwt -- отличает от access/id токенов |
iss | string | URL издателя realm'а (такой же как в access tokens) |
sub | string | ID пользователя |
realm_id | string | ID realm'а |
client_id | string | OAuth client ID |
token_jti | string | JTI оригинального access token (для аудита) |
challenge_id | string | ID MFA challenge |
purpose | string | Верифицированное назначение (напр. account.delete) |
acr | string | urn:voxkey:aal2 |
amr | array | Методы аутентификации, напр. ["pwd", "otp"] |
auth_time | number | Временная метка верификации |
iat | number | Время выпуска |
exp | number | Время истечения (iat + 300 секунд) |
jti | string | Уникальный ID токена |
TTL: 5 минут.
Валидация на вашем бэкенде
1. Получите JWKS
curl https://app.voxkey.io/oauth2/{realmUUID}/oidc/jwks
2. Валидируйте токен
Проверьте эти поля:
typдолжен бытьstep_up+jwt(предотвращает принятие обычных access tokens как step-up proof)issдолжен совпадать с URL издателя вашего realm'аexpдолжен быть в будущемsubдолжен совпадать с аутентифицированным пользователемpurposeдолжен совпадать с выполняемым действиемclient_idдолжен совпадать с вашим приложением- Подпись должна верифицироваться через JWKS по
kidиз заголовка JWT
Пример на Node.js (jose)
import * as jose from 'jose';
const JWKS = jose.createRemoteJWKSet(
new URL('https://app.voxkey.io/oauth2/{realmUUID}/oidc/jwks')
);
async function validateStepUpToken(token, expectedPurpose, expectedUserId) {
const { payload, protectedHeader } = await jose.jwtVerify(token, JWKS, {
issuer: 'https://app.voxkey.io/oauth2/{realmUUID}',
});
if (protectedHeader.typ !== 'step_up+jwt') {
throw new Error('Это не step-up токен');
}
if (payload.purpose !== expectedPurpose) {
throw new Error(`Несоответствие purpose: ожидался ${expectedPurpose}, получен ${payload.purpose}`);
}
if (payload.sub !== expectedUserId) {
throw new Error('Несоответствие пользователя');
}
return payload;
}
Пример на PHP (firebase/php-jwt)
use Firebase\JWT\JWT;
use Firebase\JWT\JWK;
$jwksJson = file_get_contents('https://app.voxkey.io/oauth2/{realmUUID}/oidc/jwks');
$jwks = json_decode($jwksJson, true);
$keys = JWK::parseKeySet($jwks);
$payload = JWT::decode($stepUpToken, $keys);
if (($payload->typ ?? null) !== 'step_up+jwt') {
throw new Exception('Это не step-up токен');
}
if ($payload->purpose !== $expectedPurpose) {
throw new Exception('Несоответствие purpose');
}
if ($payload->sub !== $expectedUserId) {
throw new Exception('Несоответствие пользователя');
}
Мягкая привязка к access token
Step-up токен содержит token_jti -- JTI access token, который использовался при создании challenge. Однако ваш бэкенд не должен проверять строгое совпадение. Пользователь может обновить свой access token между созданием challenge и использованием step-up токена.
Обязательные проверки: sub + client_id + purpose + exp
token_jti -- только для аудита и расследований.