Skip to main content

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

  • GET requires platform role Viewer or higher.
  • PUT and POST .../test require 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):

providerWhat it configurespublic_config fieldsSecret
resendTransactional email{ "from": "auth@example.com" }Resend API key (re_…)
oauth_googleGoogle sign-in{ "client_id": "…apps.googleusercontent.com" }OAuth client secret
oauth_githubGitHub sign-in{ "client_id": "Iv1.…" }OAuth client secret
oauth_microsoftMicrosoft / Entra ID sign-in{ "client_id": "…", "tenant": "common" }OAuth client secret
oauth_appleSign in with Apple{ "client_id": "com.you.web", "team_id": "…", "key_id": "…" }.p8 private key (PEM)
oauth_metaFacebook (Meta) sign-in{ "client_id": "<App ID>" }App Secret

Per-provider public_config + secret notes:

  • oauth_microsofttenant is optional (defaults to common: work/school + personal accounts). Use organizations, consumers, or a tenant GUID to restrict.
  • oauth_apple — Apple has no static secret. public_config carries the Services ID (client_id), team_id, and key_id; the secret is the .p8 private 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 the email permission, 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
}
]
}
FieldTypeMeaning
secrets_enabledbooltrue when IDENTSPHERE_CONFIG_KEY is set. When false, secret writes are rejected and the runtime uses env vars.
providers[].providerstringOne of resend, oauth_google, oauth_github.
providers[].enabledboolWhether this provider's DB row is active.
providers[].public_configobjectNon-secret config (sender address, client ID).
providers[].secret_setboolWhether a secret is stored. The value is never returned.
providers[].key_idstring|nullHex id of the encryption key the secret was sealed under. Changes after a key rotation.
providers[].updated_atstring|nullRFC3339 timestamp of the last write.
providers[].degradedbooltrue 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"
}
FieldTypeRequiredNotes
enabledboolyesActivate or deactivate the provider.
public_configobjectno (default {})Non-secret config. Must be a JSON object.
secretstringnoNew secret value. Absent / null = keep the existing secret (toggle-only save). An empty string "" is rejected — use clear_secret.
clear_secretboolno (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_config must 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_id is required when enabled: 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_KEY to be set — otherwise the write is rejected with 400.

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." }
FieldTypeMeaning
okboolWhether the probe succeeded.
detailstringHuman-readable result. Safe to surface in a UI — contains no secret material.

What each provider's test actually does:

providerProbeok: true when
resendGET https://api.resend.com/domains with the stored key (5s timeout)Resend returns 2xx
oauth_google / oauth_githubLocal check that a client ID + secret are presentBoth 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:

StatusCodeWhen
401authentication_requiredNo valid auth credential.
401mfa_requiredPUT / test without a recent MFA step-up. Re-verify MFA (within 5 min) and retry.
403forbiddenCaller lacks the required platform role (Viewer for GET, Admin for PUT/test).
403csrf_failedCookie-authed PUT/test without a valid X-IdentSphere-CSRF header. See Authentication.
400invalid_inputUnknown :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.
429rate_limitedMore than 100 requests/min from this IP.

Notes

  • The masked GET is 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: true provider means logins/email for that provider are silently using the env fallback. The same signal appears in GET /readyz under provider_config.degraded. See Operations.