Authenticators
An authenticator is a tenant-configured instance of an authentication factor type. It can stand alone, such as a TOTP factor, or point at a connection for delivery-based factors such as email or SMS one-time codes.
This release ships the authenticator foundation: the registry, encrypted configuration storage, CRUD API, and the enrollment/challenge tables that factor implementations build on. No concrete authenticator factor types are registered yet.
Authenticators vs. connections
A connection is a credentialed reference to an external provider — an SMS gateway, an email provider, an external identity provider. An authenticator is different: it's what actually challenges and verifies a user during sign-in (a TOTP code, an email one-time code, a passkey). Some authenticator types have nothing to connect to at all — TOTP and passkeys are pure local cryptography — while others (email OTP, SMS OTP, push) optionally point at a connection purely for delivery. If a factor type needs to send something, it wraps a connection; if it doesn't, it stands alone.
Multiple authenticators of the same type
Nothing stops you from creating more than one authenticator of the same
type in a tenant (the only uniqueness rule is that display names must be
unique). This is genuinely useful for delivery-based types: if you have two
email connections — say, two providers, or one per region — you'd typically
configure one email_otp authenticator per connection, each with its own
display name and delivery settings.
For non-delivery types like TOTP or passkeys, there's usually little reason to configure more than one — most tenants will only ever need a single instance of each.
Managing authenticators requires a role with the
authenticators:read/authenticators:create/authenticators:update/
authenticators:delete scopes — see Using o2idctl for how to
log in, and Managing Roles for how to create and
assign roles.
Discovering available types
o2idctl authenticators types
Each entry describes an authenticator type's type key, category, and
configSchema. The schema tells you which fields to pass via --field
when creating an authenticator, and which fields are secret.
Creating an authenticator
o2idctl authenticators create --type <a type from "authenticators types"> --display-name "Company TOTP" \
--field <configField>=value --field <secretField>=secret-value
--field is repeatable and carries both non-secret configuration and
secret values in one set. The service splits them before storing the
authenticator, encrypting only fields marked secret in the type's
configSchema. A --field value is parsed as JSON when possible (so
--field digits=6 or --field caseSensitive=true carry a number/bool
rather than a string), otherwise treated as a plain string.
The response never includes secret values:
{
"id": "a3f2d1c0b9e8d7c6a5f4e3d2c1b0a998",
"type": "...",
"category": "...",
"displayName": "Company TOTP",
"config": { "...": "..." },
"secretKeys": ["apiKey"],
"enabled": true,
"createdAt": "...",
"updatedAt": "..."
}
Delivery-based authenticators may pass --connection-id pointing at an
existing connection.
Updating and deleting
o2idctl authenticators update <id> --display-name "Production TOTP"
o2idctl authenticators delete <id>
--field on update is merged key-by-key into the existing field set, so
you can rotate one secret without resubmitting every current secret
value. --display-name, --connection-id, and --enabled only change
when explicitly supplied.
Troubleshooting
| Situation | Response |
|---|---|
displayName missing or empty | 400 — invalid authenticator display name |
type names a type nothing has registered | 400 — unknown authenticator type |
| A required field is missing | 400 — missing required field |
displayName already used by another authenticator in the tenant | 409 — authenticator display name already exists |
<id> doesn't match anything | 404 — authenticator not found |
API Reference
See the API Reference for the full request/response schemas.