Connections
A connection is O2ID's universal integration layer for external
providers — SMS, email, external identity providers, identity
verification, and more. Every connection type is registered in-process
under a unique connectorType key; a connection itself is a tenant's own
named instance of one, holding that type's non-secret configuration in
plaintext and its secret fields (API keys, OAuth client secrets, ...)
encrypted at rest.
Two connection types ship today: oidc (category identity-provider),
for federated login through an external OpenID Connect provider — see
Managing Flows for wiring
one into a login flow with a connection node — and twilio (category
sms-provider), for delivering SMS one-time codes through
the SMS OTP authenticator. A second
sms-provider connector (e.g. for Vonage) would register its own unique
type under the same category.
Beyond these compiled-in types, anyone can register a connection type backed by a remote HTTP service — no O2ID source changes or rebuild required — and use it exactly like a compiled-in one below. See Connection Types.
Managing connections requires a role with the connections:read/
connections:create/connections:update/connections:delete scopes —
see Managing Roles for how to create and assign roles.
Encryption at rest
Connection secrets are stored reversibly (not hashed, unlike user passwords and application client secrets) — a connection needs the plaintext to call the third-party service it wraps. They're encrypted with AES-256-GCM under a key derived per tenant (via HKDF, keyed by tenant ID) from a single root key, so no per-tenant key material is ever stored — a leak of one tenant's data can't be combined with another tenant's.
This is a defense-in-depth measure for this one sensitive field, not a replacement for encrypting your storage layer as a whole — that's a deployment responsibility covered in Storage.
Field-level encryption here specifically protects against someone who can query the live, running database (a SQL-injection read, an over-privileged replica, a support engineer debugging a ticket) from reading third-party API keys and OAuth client secrets in plaintext; encrypting the database's underlying storage protects everything else against a stolen disk or an unencrypted backup. Configure both.
The root key is a 256-bit value (hex- or base64-encoded), read from an environment variable at startup:
export O2ID_MASTER_KEY=$(openssl rand -hex 32)
o2id serve
o2id.toml never contains the key itself — secrets.master_key_env just
names the environment variable to read it from at startup (the shipped
config points it at O2ID_MASTER_KEY). A malformed key still fails
startup outright — there's no recovering from that, since serving with a
misconfigured key would mean no connection secret could be decrypted at
all.
If the env var is unset entirely, O2ID generates a random key and saves
it to the path in secrets.master_key_file (the shipped config sets it to
o2id.master.key, next to o2id.toml) instead of failing to start —
purely a local-development convenience, not a substitute for setting the
env var explicitly. Restarting reuses the saved key rather than
generating a new one, so existing encrypted data stays readable, but
losing that file makes every encrypted connection secret and tenant
signing key unrecoverable — never commit it to version control, and set
O2ID_MASTER_KEY explicitly for anything beyond local development.
Discovering available connection types
o2idctl connections types
Each entry describes a connection type's type key, category, and
configSchema — the field names, types, and which are secret — so you
know what to pass via --field when creating a connection of that type,
without hardcoding per-type knowledge.
Creating a connection
o2idctl connections create --type <a type from "connections types"> --name "My Connection" \
--field <configField>=value --field <secretField>=secret-value
--field is repeatable and carries both non-secret configuration and
secret values, keyed by the connection type's configSchema field keys —
the service splits them apart before persisting, encrypting only the ones
marked secret. A --field value is parsed as JSON when possible (so
--field port=8080 or --field verifyTls=true carry a number/bool
rather than a string), otherwise treated as a plain string. Every field
the connection type marks required must be present or the request is
rejected with 400.
For example, the oidc connection type's fields are issuer, clientId,
clientSecret (secret), and an optional scopes (defaults to openid email profile):
o2idctl connections create --type oidc --name "Corporate IdP" \
--field issuer=https://idp.example.com \
--field clientId=my-client-id \
--field clientSecret=my-client-secret
The response never includes secret values — only secretKeys, the list
of which secret fields are currently set:
{
"id": "9f8e7d6c5b4a3928170f6e5d4c3b2a19",
"connectorType": "...",
"category": "...",
"name": "My Connection",
"config": { "...": "..." },
"secretKeys": ["apiKey"],
"enabled": true,
"createdAt": "...",
"updatedAt": "..."
}
Updating a connection
o2idctl connections update <id> --field apiKey=rotated-value
--field on update is merged key-by-key into the connection's
existing field set, not replaced wholesale — since the API never echoes
back current secret values, you have no way to resupply every credential
just to rotate one of them or rename the connection. --name and
--enabled are likewise only changed when explicitly supplied.
Testing a connection
o2idctl connections test <id>
Resolves the connection and invokes the connection type's own live validation (e.g. an authenticated no-op call to the provider) without persisting anything:
{"valid": false, "error": "invalid credentials"}
A failed test is reported as valid: false, not a CLI/API error — a bad
API key is an expected outcome of testing a connection, not a server
error.
Listing and deleting
o2idctl connections list
o2idctl connections delete <id>
connections list lists every connection for the tenant (with secrets
masked the same way as connections get); connections delete removes
one.
API Reference
See the API Reference for the full request/response schemas.