Skip to main content
Version: Next

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.

note

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.

note

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.