Skip to main content

Managing Connections

A connection is a configured instance of a connector — O2ID's universal integration layer for external providers (SMS, email, external identity providers, identity verification, and more). Every connector type is registered in-process under a unique connectorType key; a connection is a tenant's own named instance of one, holding that connector's non-secret configuration plaintext and its secret fields (API keys, OAuth client secrets, ...) encrypted at rest.

This release ships the connections foundation — the generic data model, encryption, and CRUD API — with no concrete connector types registered yet. Concrete categories (external identity providers for federated login, SMS/email notifications, identity verification) are built on top of this foundation in later releases; each adds its own connector package and capability interface without changing the API described here.

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 connector 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

The env var name is configurable via secrets.master_key_env in o2id.toml (defaults to O2ID_MASTER_KEY). O2ID fails to start if the key is missing or malformed — there's no fallback, since serving with a misconfigured key would mean no connection secret could be decrypted at all.

Discovering available connector types

curl -s http://localhost:8080/t/system/connections/meta \
-H "Authorization: Bearer ${ACCESS_TOKEN}"

Each entry describes a connector type's type key, category, and configSchema — the field names, types, and which are secret — so a caller (or a future admin UI) knows what to pass when creating a connection of that type, without hardcoding per-connector knowledge.

Creating a connection

curl -s -X POST http://localhost:8080/t/system/connections \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"connectorType": "<a type from /connections/meta>",
"name": "My Connection",
"fields": {
"<configField>": "value",
"<secretField>": "secret-value"
}
}'

fields carries both non-secret configuration and secret values in one map, keyed by the connector type's configSchema field keys — the service splits them apart before persisting, encrypting only the ones marked secret. Every field the connector type marks required must be present or the request is rejected with 400.

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

curl -s -X PATCH http://localhost:8080/t/system/connections/<id> \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"fields": {"apiKey": "rotated-value"}}'

fields 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, a client has 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

curl -s -X POST http://localhost:8080/t/system/connections/<id>/test \
-H "Authorization: Bearer ${ACCESS_TOKEN}"

Resolves the connection and invokes the connector'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 200 with valid: false — a bad API key is an expected outcome of testing a connection, not a server error.

Listing and deleting

curl -s http://localhost:8080/t/system/connections \
-H "Authorization: Bearer ${ACCESS_TOKEN}"

curl -s -X DELETE http://localhost:8080/t/system/connections/<id> \
-H "Authorization: Bearer ${ACCESS_TOKEN}"

GET /connections lists every connection for the tenant (with secrets masked the same way as a single GET); DELETE /connections/{id} removes one.

Troubleshooting

SituationResponse
name missing or empty400invalid connection name
connectorType names a type nothing has registered400unknown connector type
A required field (per the connector type's configSchema) is missing400missing required field
name already used by another connection in the tenant409connection name already exists
<id> doesn't match anything404connection not found
O2ID_MASTER_KEY missing or malformed at startupO2ID exits immediately with an error — connections can't be encrypted or decrypted without it

API Reference

See the API Reference for the full request/response schemas.