Integrations (provider config)
Manage email + OAuth providers (Resend, Google, GitHub) at runtime, from the
admin console or directly over the API. Secrets are encrypted at rest with
IDENTSPHERE_CONFIG_KEY and consumed by the running server with no
redeploy. When a provider has no DB row (or its secret can't be decrypted),
the server falls back to the matching environment variables.
For the concepts behind this — the at-rest encryption model, key rotation, and the env fallback — see Secrets & encryption and Integrations.
::: tip Auth
GETrequires platform role Viewer or higher.PUTandPOST .../testrequire platform role Admin or higher and a recent MFA step-up (you must have completed MFA within the last 5 minutes).
All three are rate-limited to 100 requests/min/IP. :::
Known providers (the :provider path segment):
provider | What it configures | public_config fields | Secret |
|---|---|---|---|
resend | Transactional email | { "from": "auth@example.com" } | Resend API key (re_…) |
oauth_google | Google sign-in | { "client_id": "…apps.googleusercontent.com" } | OAuth client secret |
oauth_github | GitHub sign-in | { "client_id": "Iv1.…" } | OAuth client secret |
oauth_microsoft | Microsoft / Entra ID sign-in | { "client_id": "…", "tenant": "common" } | OAuth client secret |
oauth_apple | Sign in with Apple | { "client_id": "com.you.web", "team_id": "…", "key_id": "…" } | .p8 private key (PEM) |
oauth_meta | Facebook (Meta) sign-in | { "client_id": "<App ID>" } | App Secret |
Per-provider public_config + secret notes:
oauth_microsoft—tenantis optional (defaults tocommon: work/school + personal accounts). Useorganizations,consumers, or a tenant GUID to restrict.oauth_apple— Apple has no static secret.public_configcarries the Services ID (client_id),team_id, andkey_id; the secret is the.p8private key (PEM). IdentSphere mints the short-lived ES256 client-secret JWT per token exchange. The Test action validates the key by minting it.oauth_meta— the app must be granted theemailpermission, or sign-in fails (no email returned).
Microsoft, Apple, and Meta are dashboard-only (no env-var fallback); Google
and GitHub additionally support IDENTSPHERE_OAUTH_* env vars.
GET /v1/admin/integrations
Masked status of every provider. Never returns secret material — only whether a secret is stored.
Request
GET /v1/admin/integrations
Response
200 OK
{
"secrets_enabled": true,
"providers": [
{
"provider": "resend",
"enabled": true,
"public_config": { "from": "auth@example.com" },
"secret_set": true,
"key_id": "a1b2c3d4",
"updated_at": "2026-06-01T12:00:00+00:00",
"degraded": false
},
{
"provider": "oauth_google",
"enabled": false,
"public_config": {},
"secret_set": false,
"key_id": null,
"updated_at": null,
"degraded": false
},
{
"provider": "oauth_github",
"enabled": false,
"public_config": {},
"secret_set": false,
"key_id": null,
"updated_at": null,
"degraded": false
}
]
}
| Field | Type | Meaning |
|---|---|---|
secrets_enabled | bool | true when IDENTSPHERE_CONFIG_KEY is set. When false, secret writes are rejected and the runtime uses env vars. |
providers[].provider | string | One of resend, oauth_google, oauth_github. |
providers[].enabled | bool | Whether this provider's DB row is active. |
providers[].public_config | object | Non-secret config (sender address, client ID). |
providers[].secret_set | bool | Whether a secret is stored. The value is never returned. |
providers[].key_id | string|null | Hex id of the encryption key the secret was sealed under. Changes after a key rotation. |
providers[].updated_at | string|null | RFC3339 timestamp of the last write. |
providers[].degraded | bool | true when the row is enabled but its secret could not be applied (key unset / rotated away / missing fields) — the runtime is using the env fallback. A red flag after a botched key rotation. |
PUT /v1/admin/integrations/:provider
Upsert one provider's config. Requires Admin + a recent MFA step-up.
Request
PUT /v1/admin/integrations/:provider
{
"enabled": true,
"public_config": { "from": "auth@example.com" },
"secret": "re_xxxxxxxxxxxxxxxxxxxx"
}
| Field | Type | Required | Notes |
|---|---|---|---|
enabled | bool | yes | Activate or deactivate the provider. |
public_config | object | no (default {}) | Non-secret config. Must be a JSON object. |
secret | string | no | New secret value. Absent / null = keep the existing secret (toggle-only save). An empty string "" is rejected — use clear_secret. |
clear_secret | bool | no (default false) | true removes the stored secret. |
Secret-update semantics — the three intents:
| You want to… | Send |
|---|---|
| Save a new secret | "secret": "re_…" |
Leave the secret unchanged (e.g. just flip enabled) | omit secret |
| Delete the stored secret | "clear_secret": true |
Server-side validation:
public_configmust be a JSON object.resend:from, when present, must be a valid email address (validated even on a toggle-off save — a spoofed sender on an auth server is a phishing lever).oauth_google/oauth_github:client_idis required whenenabled: true.- Secret format (defense-in-depth): Resend keys must start with
re_; OAuth client secrets must be ≥ 8 chars; any secret is capped at 4096 bytes. - Storing a secret requires
IDENTSPHERE_CONFIG_KEYto be set — otherwise the write is rejected with400.
Response
200 OK
Returns the updated masked view of the provider —
same shape as one element of the GET providers array. The write triggers an
immediate in-process reload, so the new config is live on this replica
instantly and on others within the reload TTL (~30s).
POST /v1/admin/integrations/:provider/test
Probe the stored credentials. Requires Admin + a recent MFA step-up (testing is a credential-validity oracle, so it's gated as tightly as a write). Never echoes the secret or any upstream response body.
Request
POST /v1/admin/integrations/:provider/test
No body.
Response
200 OK
{ "ok": true, "detail": "Resend API key is valid." }
| Field | Type | Meaning |
|---|---|---|
ok | bool | Whether the probe succeeded. |
detail | string | Human-readable result. Safe to surface in a UI — contains no secret material. |
What each provider's test actually does:
provider | Probe | ok: true when |
|---|---|---|
resend | GET https://api.resend.com/domains with the stored key (5s timeout) | Resend returns 2xx |
oauth_google / oauth_github | Local check that a client ID + secret are present | Both are stored (the full OAuth handshake is verified at sign-in, not here) |
Example failures (still 200 OK, with ok: false):
{ "ok": false, "detail": "Resend is not configured (save an API key first)." }
{ "ok": false, "detail": "Resend rejected the key (HTTP 401)." }
{ "ok": false, "detail": "Could not reach the Resend API." }
Error responses
Shared by all three endpoints:
| Status | Code | When |
|---|---|---|
| 401 | authentication_required | No valid auth credential. |
| 401 | mfa_required | PUT / test without a recent MFA step-up. Re-verify MFA (within 5 min) and retry. |
| 403 | forbidden | Caller lacks the required platform role (Viewer for GET, Admin for PUT/test). |
| 403 | csrf_failed | Cookie-authed PUT/test without a valid X-IdentSphere-CSRF header. See Authentication. |
| 400 | invalid_input | Unknown :provider; public_config not an object; bad from; missing client_id when enabling OAuth; malformed secret; empty-string secret; or storing a secret while IDENTSPHERE_CONFIG_KEY is unset. |
| 429 | rate_limited | More than 100 requests/min from this IP. |
Notes
- The masked
GETis intentionally the only read path — there is no endpoint that returns a stored secret. Once written, a secret can only be overwritten or cleared, never read back. - Every write and test is recorded in the audit log with the provider, the actor, and the key id — never the secret value.
- A
degraded: trueprovider means logins/email for that provider are silently using the env fallback. The same signal appears inGET /readyzunderprovider_config.degraded. See Operations.