Skip to main content

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.

Drivers carry behavior, not just data

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.

List authenticator driversGET /api/authenticator-drivers
GET /api/authenticator-drivers requires a superuser. Returns the registered MFA driver definitions (HAL rel inf:mfa-drivers). Out of the box only `totp` ships. Driver objects carry behavior functions that are dropped by JSON serialization, so each driver serializes to { id, name, verifyComponent, setupComponent }. Use a driver `id` as the `id` when enabling an authenticator.
Response · 200
[
{
"id": "totp",
"name": "TOTP",
"verifyComponent": "totpVerify",
"setupComponent": "totpSetup"
}
]
Captured from the API examples test suite; ids and timestamps are normalized.

GET /api/authenticator-drivers/{id}

Get a single MFA driver definition.

Authentication: Superuser

Path Parameters:

ParameterTypeDescription
idstringDriver 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).

Get an authenticator driverGET /api/authenticator-drivers/totp
GET /api/authenticator-drivers/{id} requires a superuser and resolves the driver via the `mfa` driver manager. This route has no HAL plugin, so the driver is returned as a bare object ({ id, name, verifyComponent, setupComponent } after functions are stripped by JSON). An UNKNOWN driver id returns 400 (boom.badRequest from the driver manager), NOT 404.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/authenticator-drivers/totp"
}
},
"id": "totp",
"name": "TOTP",
"verifyComponent": "totpVerify",
"setupComponent": "totpSetup"
}
Captured from the API examples test suite; ids and timestamps are normalized.
Unknown driver id returns 400

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:

FieldTypeRequiredDescription
idstringNoA 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.

Enable an authenticatorPOST /api/authenticators
POST /api/authenticators requires a superuser. Payload is { id } where `id` is a registered driver id; a missing or unknown id yields 400. The handler does MFA.findOrCreate keyed on the driver, so it is idempotent (unlike the PUT bulk enable). Responds 200 (NOT 201) with the authenticator row (whose own `id` IS the driver id) and embeds inf:driver. No Location header is emitted.
Request body
{
"id": "totp"
}
Response · 200
{
"_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"
}
}
}
Captured from the API examples test suite; ids and timestamps are normalized.
Idempotent single-enable

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:

FieldTypeRequiredDescription
authenticatorsobjectYesAn 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.

Bulk enable / disable authenticatorsPUT /api/authenticators
PUT /api/authenticators requires a superuser. Payload is { authenticators } where `authenticators` is an OBJECT MAP of driverId -> boolean. Truthy entries are enabled (bulk-created); falsy entries are disabled (removed), and the driver’s cleanup() purges each user’s stored secret for that mechanism. Returns 200 with an EMPTY body. Runs in a route transaction. NOTE: 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.
Request body
{
"authenticators": {
"totp": true
}
}
Response · 200 · no body
Captured from the API examples test suite; ids and timestamps are normalized.
Enable path is a plain insert

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:

ParameterTypeDescription
idstringThe 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.

Disable an authenticatorDELETE /api/authenticators/totp
DELETE /api/authenticators/{id} requires a superuser. The id segment is the driver id (the authenticator’s PK). If the authenticator exists, its driver cleanup() runs then the row is destroyed; the handler returns h.continue, so the route answers 200 with an EMPTY body (NOT 204). Deleting a non-existent authenticator is a no-op that still returns 200 empty. Runs in a route transaction.
Response · 200 · no body
Captured from the API examples test suite; ids and timestamps are normalized.

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.

List domain-eligible authenticatorsGET /api/domain-eligible-authenticators
GET /api/domain-eligible-authenticators does NOT require a superuser. It 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). HAL rel inf:domain-eligible-authenticators; each row embeds inf:driver.
Response · 200
[
{
"id": "totp",
"name": "TOTP",
"tenant": "acme",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
}
]
Captured from the API examples test suite; ids and timestamps are normalized.

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 {}.

List enabled authenticatorsGET /api/authenticators-list
GET /api/authenticators-list is a fast flat list (HAL rel inf:authenticators-list) of the enabled authenticators, built by a raw SQL select of id, name, createdAt, updatedAt. Each row carries a `permissions` object from req.permissions('mfa', row); there is no `mfa` permission driver registered, so it is always {}. The query has no ORDER BY (the fixture is sorted by id for stable capture).
Response · 200
[
{
"id": "totp",
"name": "TOTP",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"permissions": {}
}
]
Captured from the API examples test suite; ids and timestamps are normalized.
No guaranteed order

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.