Step 4: Handle the MFA response
⏱ 5 minProcess the MFA verification result and implement your security logic
MFA Response Structure
interface IdpAuthResponse {
success: boolean;
sid?: string; // Step-up Session ID (on success)
error?: string; // Error message (on failure)
timestamp: number; // Unix timestamp in milliseconds
action?: string; // Requested action type
}Basic Handling
const mfaResult = await transcodes.openAuthIdpModal({
resource: 'sensitive_action',
action: 'delete',
});
if (mfaResult.success && mfaResult.payload[0]?.success) {
const { sid, timestamp } = mfaResult.payload[0];
console.log(`Verified at ${new Date(timestamp)}`);
if (sid) console.log('Step-up Session ID:', sid);
// Proceed with protected action
await performSensitiveAction();
} else {
// User cancelled or verification failed
console.log('MFA verification not completed');
}MFA Verification Token
After successful MFA, send both the access token and the step-up sid to your backend. The access token proves identity; the sid proves the user just completed step-up for this specific (resource, action).
const mfaResult = await transcodes.openAuthIdpModal({
resource: 'admin_settings',
action: 'update',
});
if (mfaResult.success && mfaResult.payload[0]?.success) {
const stepUpSid = mfaResult.payload[0].sid;
const token = await transcodes.token.getAccessToken();
const response = await fetch('/api/admin/settings', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ stepUpSid, setting: 'value' }),
});
}Treat sid as untrusted on the client. Your backend must re-verify it
against the Transcodes API before performing the protected action — see
Server-Side Verification below.
Server-Side Verification
There are two independent server-side checks for a step-up flow, and you should run both:
- Access token (JWT) — proves who the caller is. See Server-Side Integration for the full JWT verification setup.
- Step-up
sid— proves that the same caller just completed a step-up for the requested(resource, action). Verify it against the Transcodes API.
Verifying the step-up sid
Forward the sid from openAuthIdpModal to your backend, then call:
GET https://api.transcodesapis.com/v1/auth/temp-session/step-up/{sid}- Header:
x-transcodes-token: <AUTH_API_TOKEN>— a server-only JWT issued from the Transcodes Console (different fromTRANSCODES_TOKENused by the MCP server). Store it asTRANSCODES_AUTH_API_TOKENand never expose it to the browser.
// app/api/admin/settings/route.ts (Next.js App Router)
import { NextResponse } from 'next/server';
const AUTH_API_TOKEN = process.env.TRANSCODES_AUTH_API_TOKEN!;
export async function POST(req: Request) {
const accessToken = req.headers
.get('authorization')
?.replace(/^Bearer\s+/i, '');
const user = await verifyAccessToken(accessToken);
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { stepUpSid, ...body } = await req.json();
if (!stepUpSid) {
return NextResponse.json(
{ error: 'Missing step-up session id' },
{ status: 400 },
);
}
const response = await fetch(
`https://api.transcodesapis.com/v1/auth/temp-session/step-up/${stepUpSid}`,
{
headers: {
'x-transcodes-token': AUTH_API_TOKEN,
},
},
);
if (!response.ok) {
return NextResponse.json({
logId: crypto.randomUUID(),
success: false,
statusCode: response.status,
payload: null,
error: 'Failed to verify step-up session',
});
}
const data = await response.json();
// data.payload[0] => { project_id, member_id, action, role }
// Optional: assert payload[0].member_id === user.id and payload[0].action matches.
await updateSettings(body);
return NextResponse.json({ success: true });
}Example successful response from temp-session/step-up/{sid}:
{
"logId": "01KPP893GSV54TKVST53MV7BB1",
"success": true,
"statusCode": 200,
"payload": [
{
"project_id": "ca3da7ff8dd4391d8d80ad01",
"member_id": "ca3db8488dd4391d8d80ad04",
"action": "read",
"role": "admin"
}
],
"error": null
}Never call the temp-session/step-up/{sid} endpoint from the browser, and
never embed AUTH_API_TOKEN in client code — it is a server secret.
Best Practices
Security Recommendations:
- Always verify server-side — Re-check the access token (JWT) and the step-up
sidagainst the Transcodes API before performing any sensitive action. Do not trust the client result alone. - Treat
sidas one-shot — Forward it to your backend immediately and let Transcodes decide if the step-up is still valid; never cache step-up state in client memory. - Log MFA events — Use
trackUserAction()so success and failure both appear in the Audit Logs.
History Log
Record MFA and sensitive actions for audit logs using trackUserAction(). Events appear in the Audit Logs panel
// MFA verification success
if (mfaResult.success && mfaResult.payload[0]?.success) {
await transcodes.trackUserAction({
tag: 'admin:access',
severity: 'high',
status: true,
metadata: { action: 'read' },
});
}
// Sensitive action with webhook alert
await transcodes.trackUserAction(
{ tag: 'payment:transfer', severity: 'high', status: true, metadata: { amount: 1000 } },
{ webhookNotification: true }
);trackUserAction tag | Typical use (audit log) |
|---|---|
admin:access | Admin panel access |
user:delete | Account deletion |
document:delete | Document deletion |
payment:transfer | Financial transaction |
These tags are for trackUserAction() only. For openAuthIdpModal(), use resource + action (RBAC)—see Modal API.
See Audit API for full trackUserAction reference
Handle account without MFA
If the signed-in member has not set up MFA yet:
const mfaResult = await transcodes.openAuthIdpModal({
resource: 'sensitive_action',
action: 'delete',
forceStepUp: true,
});
if (!mfaResult.success || !mfaResult.payload[0]?.success) {
// Redirect to MFA setup in console panel
alert('Please set up MFA first');
await transcodes.openAuthConsoleModal(); // Opens console panel for MFA setup
}