Multi-Factor Auth
Endpoints for managing multi-factor authentication on a tenant: the catalog of registered MFA drivers and the Authenticators a tenant has enabled.
A driver is a registered MFA mechanism definition (for example totp), held
in the mfa driver manager. Out of the box only the totp driver ships. An
Authenticator is a persisted MFA row whose primary key id IS the driver id
it was created from, so a tenant can have at most one Authenticator per driver.
Every route except GET /api/domain-eligible-authenticators requires a
Superuser (auth.assertSuperuser). There is no license feature gate on this
module.
Driver objects carry behavior functions (setup, verify, confirmSetup,
cleanup, isDomainEligible, hasData, hasTempData). JSON serialization
drops the functions, so a driver always serializes to its plain data fields:
{ id, name, verifyComponent, setupComponent }.
GET /api/authenticator-drivers
List the registered MFA driver definitions.
Authentication: Superuser
Response:
A HAL collection (rel inf:mfa-drivers) of the registered drivers. Out of the
box only totp ships. Each driver serializes to { id, name, verifyComponent, setupComponent }. Use a driver id as the id when enabling an Authenticator.
[
{
"id": "totp",
"name": "TOTP",
"verifyComponent": "totpVerify",
"setupComponent": "totpSetup"
}
]
GET /api/authenticator-drivers/{id}
Get a single MFA driver definition.
Authentication: Superuser
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Driver id (for example totp) |
Response:
This route has no HAL plugin, so the driver is returned as a bare object
({ id, name, verifyComponent, setupComponent } after the functions are
stripped by JSON).
{
"_links": {
"self": {
"href": "https://informer.example.com/api/authenticator-drivers/totp"
}
},
"id": "totp",
"name": "TOTP",
"verifyComponent": "totpVerify",
"setupComponent": "totpSetup"
}
An unknown driver id resolves through the mfa driver manager, which throws
boom.badRequest. The route answers 400, NOT 404.
POST /api/authenticators
Enable (register) a single Authenticator.
Authentication: Superuser
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
id | string | No | A registered driver id. A missing or unknown id yields 400. |
The handler does a findOrCreate keyed on the resolved driver, so it is
idempotent: enabling an already-enabled Authenticator is safe and returns the
existing row.
Example Request:
{ "id": "totp" }
Response:
Responds 200 (NOT 201) with the Authenticator row, whose own id IS the
driver id. The body embeds the resolved driver under inf:driver. No Location
header is emitted.
{
"id": "totp"
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/authenticators"
}
},
"id": "totp",
"name": "TOTP",
"tenant": "acme",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"_embedded": {
"inf:driver": {
"_links": {
"self": {
"href": "https://informer.example.com/mfa-drivers/totp"
}
},
"id": "totp",
"name": "TOTP",
"verifyComponent": "totpVerify",
"setupComponent": "totpSetup"
}
}
}
Use this route, not PUT /api/authenticators, to enable one mechanism safely.
The PUT enable path is a plain insert and will raise a duplicate-key error if
the Authenticator already exists.
PUT /api/authenticators
Bulk enable or disable Authenticators by a map.
Authentication: Superuser
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
authenticators | object | Yes | An object map of driverId to boolean. Truthy entries are enabled (bulk-created); falsy entries are disabled (removed). |
Disabling an entry also runs the driver's cleanup(), which purges each user's
stored secret for that mechanism. Runs in a route transaction.
Example Request:
{ "authenticators": { "totp": true } }
Response:
Responds 200 with an EMPTY body.
{
"authenticators": {
"totp": true
}
}
The enable path is a plain insert, not an upsert, so enabling an already-enabled
Authenticator raises a duplicate-key error. Use POST /api/authenticators for
the idempotent single-enable.
DELETE /api/authenticators/{id}
Disable (remove) a single Authenticator.
Authentication: Superuser
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | The driver id (the Authenticator's primary key) |
If the Authenticator exists, its driver cleanup() runs and then the row is
destroyed. Deleting a non-existent Authenticator is a no-op. Runs in a route
transaction.
Response:
Responds 200 with an EMPTY body (NOT 204), whether the Authenticator existed
or not.
GET /api/domain-eligible-authenticators
List the Authenticators eligible for the caller's own Domain.
Authentication: Required (no Superuser needed)
Response:
This route resolves the caller's own Domain (via auth.user, then
domain.lookup) and returns the enabled Authenticators whose driver reports
isDomainEligible(domain) true. The totp driver is eligible for the built-in
local Domain (and AD/LDAP). Returns a HAL collection (rel
inf:domain-eligible-authenticators); each row embeds the resolved driver under
inf:driver.
[
{
"id": "totp",
"name": "TOTP",
"tenant": "acme",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
}
]
GET /api/authenticators-list
Get a fast flat list of the enabled Authenticators.
Authentication: Superuser
Response:
A flat list (HAL rel inf:authenticators-list) of the enabled Authenticators,
built by a raw SQL select of id, name, createdAt, and updatedAt. Each
row carries a permissions object from req.permissions('mfa', row). There is
no mfa permission driver registered, so permissions is always {}.
[
{
"id": "totp",
"name": "TOTP",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"permissions": {}
}
]
The underlying query has no ORDER BY, so the row order is not guaranteed.
Sort client-side if you need a stable order.
Not captured: live TOTP enrollment and verification
There are no binary, streaming, or live-external endpoints in this module. The
live TOTP enrollment and verification flow (the driver's setup() generates a
per-user secret and QR data URL; confirmSetup() and verify() check a
time-based OTP token) lives on the user profile and login surfaces, NOT on these
admin routes. That flow is inherently non-deterministic (a random secret and a
time-based code), so it is intentionally out of scope here and shown without a
captured example.