Headers, payloads et réponses
Créer une session MFA
- Méthode
- POST
- Endpoint
/api/v1/mfa/sessions
- Headers
-
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY
Payload attendu
{
"external_user_id": "user_123",
"email": "developer@example.com",
"roles": ["ROLE_ADMIN"],
"success_url": "https://client.example.com/mfa/success?state=STATE",
"cancel_url": "https://client.example.com/mfa/cancel?state=STATE",
"country_code": "MG"
}
Réponse JSON attendue
{
"success": true,
"data": {
"session_id": "mfa_sess_xxx",
"status": "challenge_required",
"requirement": "required",
"next_action": "challenge_required",
"hosted_url": "https://api.example.com/mfa/s/mfa_sess_xxx",
"expires_at": "2026-06-19T10:30:00+03:00",
"verification_token": null
}
}
Introspecter une session MFA
- Méthode
- POST
- Endpoint
/api/v1/mfa/sessions/introspect
- Headers
-
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY
Payload attendu
{
"session_id": "mfa_sess_xxx",
"verification_token": "mfa_ver_xxx"
}
Réponse JSON attendue
{
"success": true,
"data": {
"valid": true,
"session_id": "mfa_sess_xxx",
"status": "verified",
"verified_at": "2026-06-19T10:13:48+03:00",
"external_user_id": "user_123",
"email": "developer@example.com",
"roles": ["ROLE_ADMIN"],
"mfa_enabled": true,
"application": {
"id": 2,
"name": "Application cliente"
}
}
}
Réponse d’erreur possible
{
"success": true,
"data": {
"valid": false,
"reason": "invalid_verification_token"
}
}
Format d’erreur API standard
- Méthode
- POST
- Endpoint
/api/v1/mfa/sessions
- Headers
-
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY
Payload attendu
{
"external_user_id": "",
"email": "developer@example.com",
"roles": []
}
Réponse d’erreur possible
{
"success": false,
"error": {
"code": "validation_error",
"message": "The field external_user_id is required.",
"details": {
"field": "external_user_id"
}
}
}
Exemples de code complets
Node.js natif
import http from 'node:http';
import crypto from 'node:crypto';
const API_BASE_URL = process.env.API_BASE_URL ?? 'https://api.example.com';
const API_KEY = process.env.API_KEY ?? 'YOUR_API_KEY';
const pendingStates = new Map();
async function tanaMfa(path, options = {}) {
const response = await fetch(`${API_BASE_URL}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`,
...(options.headers ?? {}),
},
});
const payload = await response.json();
if (!response.ok || payload.success === false) {
throw new Error(payload.error?.message ?? 'Tana MFA API error');
}
return payload.data;
}
function redirect(res, location) {
res.writeHead(302, { Location: location });
res.end();
}
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, 'https://client.example.com');
if (req.method === 'POST' && url.pathname === '/login') {
const externalUserId = 'user_123';
const email = 'developer@example.com';
const state = crypto.randomBytes(32).toString('hex');
pendingStates.set(state, { externalUserId, email });
const session = await tanaMfa('/api/v1/mfa/sessions', {
method: 'POST',
body: JSON.stringify({
external_user_id: externalUserId,
email,
roles: ['ROLE_ADMIN'],
success_url: `https://client.example.com/mfa/success?state=${state}`,
cancel_url: `https://client.example.com/mfa/cancel?state=${state}`,
}),
});
return redirect(res, session.hosted_url);
}
if (req.method === 'GET' && url.pathname === '/mfa/success') {
const state = url.searchParams.get('state');
const pending = pendingStates.get(state);
if (!pending) {
res.writeHead(403);
return res.end('Invalid state');
}
const introspection = await tanaMfa('/api/v1/mfa/sessions/introspect', {
method: 'POST',
body: JSON.stringify({
session_id: url.searchParams.get('mfa_session'),
verification_token: url.searchParams.get('mfa_token'),
}),
});
if (!introspection.valid || introspection.external_user_id !== pending.externalUserId) {
res.writeHead(403);
return res.end('Invalid MFA session');
}
pendingStates.delete(state);
res.writeHead(200);
return res.end('Authenticated');
}
res.writeHead(404);
res.end('Not found');
});
server.listen(3000);
Express.js
import express from 'express';
import session from 'express-session';
import crypto from 'node:crypto';
const app = express();
const API_BASE_URL = process.env.API_BASE_URL ?? 'https://api.example.com';
const API_KEY = process.env.API_KEY ?? 'YOUR_API_KEY';
app.use(express.json());
app.use(session({
secret: process.env.WEBHOOK_SECRET ?? 'YOUR_SECRET',
resave: false,
saveUninitialized: false,
}));
async function tanaMfa(path, body) {
const response = await fetch(`${API_BASE_URL}${path}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`,
},
body: JSON.stringify(body),
});
const payload = await response.json();
if (!response.ok || payload.success === false) {
throw new Error(payload.error?.message ?? 'Tana MFA API error');
}
return payload.data;
}
app.post('/login', async (req, res, next) => {
try {
const user = { id: 'user_123', email: 'developer@example.com', roles: ['ROLE_ADMIN'] };
const state = crypto.randomBytes(32).toString('hex');
req.session.pendingMfa = { state, externalUserId: user.id };
const session = await tanaMfa('/api/v1/mfa/sessions', {
external_user_id: user.id,
email: user.email,
roles: user.roles,
success_url: `https://client.example.com/mfa/success?state=${state}`,
cancel_url: `https://client.example.com/mfa/cancel?state=${state}`,
});
res.redirect(session.hosted_url);
} catch (error) {
next(error);
}
});
app.get('/mfa/success', async (req, res, next) => {
try {
const pending = req.session.pendingMfa;
if (!pending || req.query.state !== pending.state) {
return res.status(403).send('Invalid state');
}
const introspection = await tanaMfa('/api/v1/mfa/sessions/introspect', {
session_id: req.query.mfa_session,
verification_token: req.query.mfa_token,
});
if (!introspection.valid || introspection.external_user_id !== pending.externalUserId) {
return res.status(403).send('Invalid MFA session');
}
delete req.session.pendingMfa;
req.session.user = { externalUserId: introspection.external_user_id };
res.redirect('/dashboard');
} catch (error) {
next(error);
}
});
app.listen(3000);
Fastify
import Fastify from 'fastify';
import cookie from '@fastify/cookie';
import session from '@fastify/session';
import crypto from 'node:crypto';
const app = Fastify();
const API_BASE_URL = process.env.API_BASE_URL ?? 'https://api.example.com';
const API_KEY = process.env.API_KEY ?? 'YOUR_API_KEY';
await app.register(cookie);
await app.register(session, {
secret: process.env.WEBHOOK_SECRET ?? 'a-secret-with-at-least-32-characters',
cookie: { secure: true, sameSite: 'lax' },
});
async function tanaMfa(path, body) {
const response = await fetch(`${API_BASE_URL}${path}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`,
},
body: JSON.stringify(body),
});
const payload = await response.json();
if (!response.ok || payload.success === false) {
throw new Error(payload.error?.message ?? 'Tana MFA API error');
}
return payload.data;
}
app.post('/login', async (request, reply) => {
const user = { id: 'user_123', email: 'developer@example.com', roles: ['ROLE_ADMIN'] };
const state = crypto.randomBytes(32).toString('hex');
request.session.pendingMfa = { state, externalUserId: user.id };
const session = await tanaMfa('/api/v1/mfa/sessions', {
external_user_id: user.id,
email: user.email,
roles: user.roles,
success_url: `https://client.example.com/mfa/success?state=${state}`,
cancel_url: `https://client.example.com/mfa/cancel?state=${state}`,
});
return reply.redirect(session.hosted_url);
});
app.get('/mfa/success', async (request, reply) => {
const pending = request.session.pendingMfa;
if (!pending || request.query.state !== pending.state) {
return reply.code(403).send({ error: 'Invalid state' });
}
const introspection = await tanaMfa('/api/v1/mfa/sessions/introspect', {
session_id: request.query.mfa_session,
verification_token: request.query.mfa_token,
});
if (!introspection.valid || introspection.external_user_id !== pending.externalUserId) {
return reply.code(403).send({ error: 'Invalid MFA session' });
}
request.session.pendingMfa = null;
request.session.user = { externalUserId: introspection.external_user_id };
return reply.redirect('/dashboard');
});
await app.listen({ port: 3000 });
NestJS
import { Body, Controller, Get, Injectable, Post, Query, Redirect, Req, ForbiddenException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { randomBytes } from 'node:crypto';
@Injectable()
export class TanaMfaService {
constructor(private readonly config: ConfigService) {}
private async post(path: string, body: unknown) {
const response = await fetch(`${this.config.get('API_BASE_URL') ?? 'https://api.example.com'}${path}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.config.get('API_KEY') ?? 'YOUR_API_KEY'}`,
},
body: JSON.stringify(body),
});
const payload = await response.json();
if (!response.ok || payload.success === false) {
throw new Error(payload.error?.message ?? 'Tana MFA API error');
}
return payload.data;
}
createSession(data: Record<string, unknown>) {
return this.post('/api/v1/mfa/sessions', data);
}
introspect(data: Record<string, unknown>) {
return this.post('/api/v1/mfa/sessions/introspect', data);
}
}
@Controller()
export class AuthController {
constructor(private readonly tanaMfa: TanaMfaService) {}
@Post('/login')
@Redirect()
async login(@Req() req: any, @Body() body: any) {
const user = { id: 'user_123', email: 'developer@example.com', roles: ['ROLE_ADMIN'] };
const state = randomBytes(32).toString('hex');
req.session.pendingMfa = { state, externalUserId: user.id };
const session = await this.tanaMfa.createSession({
external_user_id: user.id,
email: user.email,
roles: user.roles,
success_url: `https://client.example.com/mfa/success?state=${state}`,
cancel_url: `https://client.example.com/mfa/cancel?state=${state}`,
});
return { url: session.hosted_url };
}
@Get('/mfa/success')
async success(@Req() req: any, @Query() query: any) {
const pending = req.session.pendingMfa;
if (!pending || query.state !== pending.state) {
throw new ForbiddenException('Invalid state');
}
const introspection = await this.tanaMfa.introspect({
session_id: query.mfa_session,
verification_token: query.mfa_token,
});
if (!introspection.valid || introspection.external_user_id !== pending.externalUserId) {
throw new ForbiddenException('Invalid MFA session');
}
delete req.session.pendingMfa;
req.session.user = { externalUserId: introspection.external_user_id };
return { authenticated: true };
}
}