
# Create or replace the OIDC configuration of a connection

`PUT /v1/connections/{connection_id}/oidc` · permission `connections:write`

Creates or fully replaces the relying-party configuration of an enterprise OIDC connection.

- **First write:** send no `If-Match` header and give `sp_host` (an active host of this environment). The callback `https://<sp_host>/sso/<connection_id>/callback` is fixed from then on. The response is `201 Created`. Sending `If-Match` while no configuration exists fails with `412`.
- **Update:** `If-Match` is required. Without it the request fails with `428` and nothing is written, so a request without the header can never overwrite an existing configuration. If the configuration changed since you read it (or the value does not match) the request fails with `412`. `*` matches the current configuration. `sp_host` must be empty or the stored host.

The client secret is write-only: it is never returned, logged or written to the audit log. When you update without `client_secret`, the stored secret is kept; `private_key_jwt` removes it (the environment's signing key signs the client assertion). A changed `issuer` clears the stored discovery document; a changed `issuer` or `client_id` invalidates the last passed connection test. Sending the same configuration writes nothing. Writes are audited as `connection.protocol_config_created` or `connection.oidc_config_updated` (changed fields only; the secret appears only as `client_secret_set`).

On an **active** connection, an update that changes `issuer` or `client_id` is not applied. The whole request (a new secret included) becomes a [draft revision](../connections/revisions.md): the response is `200 OK` with the unchanged live configuration, its unchanged `ETag` and a `revision` field. Test the draft with [Test a connection](../connections/test.md) and `revision_id`, then [publish it](../connections/publish-revision.md). Other updates apply directly.

## Request

| Field | Type | Required | Description |
|---|---|---|---|
| `connection_id` | path | yes | Enterprise OIDC connection ID (`conn_…`). |
| `If-Match` | header | on update | The `ETag` from the last read or write. Missing on update: `428`; stale: `412`. Not sent on the first write. |
| `sp_host` | string | on first write | Active host of this environment (lower case). On update empty or the stored host. |
| `issuer` | string | yes | Absolute `https` URL without query or fragment, at most 512 bytes. |
| `client_id` | string | yes | 1–255 bytes, no control characters. |
| `client_auth_method` | string | yes | `private_key_jwt` (recommended; no secret is stored), `client_secret_basic` or `client_secret_post`. |
| `client_secret` | string | no | Write-only, at most 1024 bytes. Kept when omitted on update. Not allowed with `private_key_jwt`. |
| `client_secret_expires_at` | string \| null | no | When the identity provider expires the secret (RFC 3339). Only together with `client_secret`. |
| `provider_profile` | string | no | `entra`, `okta`, `google`, `keycloak`, `ping`, `onelogin`, `jumpcloud` or `generic` (default). |
| `entra_tenant_id` | string | for `entra` | The Microsoft Entra tenant ID (UUID); only for the `entra` profile. |

## Response

`201 Created` on the first write, `200 OK` on an update, with the configuration (see [Get the OIDC configuration](get.md)) and the new `ETag`. The client secret is not in the response. When the update became a draft revision, the body also has `revision` (the draft) and the configuration and `ETag` are the live ones.

## Errors

| HTTP | code | When |
|---|---|---|
| 404 | `not_found` | The connection does not exist in this environment or is not an enterprise connection. |
| 409 | `conflict` | The connection is not an OIDC connection, `sp_host` differs from the stored host, or a secret method has no stored or given secret. |
| 412 | `precondition_failed` | `If-Match` does not match the current version, the configuration changed concurrently, or `If-Match` was sent before the first write. |
| 422 | `validation_failed` | A field breaks the rules above; `errors[]` points at the field (`/issuer`, `/client_id`, `/client_auth_method`, `/client_secret`, `/client_secret_expires_at`, `/provider_profile`, `/entra_tenant_id`, `/sp_host`). |
| 428 | `precondition_required` | The configuration exists and the `If-Match` header is missing. |
| 400 | `unknown_field` | The body has an unknown field. |
| 401 | `unauthenticated` | The credential is missing, invalid or revoked. |
| 403 | `insufficient_permissions` | The credential lacks the required permission. |
| 429 | `rate_limited` | The rate limit was exceeded; retry after the `Retry-After` period. |
| 503 | `service_unavailable` | A dependency is temporarily unavailable; retry after the `Retry-After` period. |

## Example

Keep the secret in a file, not on the command line (`oidc.json`):

```json
{"sp_host":"acme.eu1.keenauth.app","issuer":"https://idp.example.com","client_id":"keenauth-acme","client_auth_method":"client_secret_basic","client_secret":"<secret from the identity provider>"}
```

```bash
curl -X PUT "https://api.eu.keenauth.com/v1/connections/conn_01j9xp6q0r1s5t7v9w1x3y5z7b/oidc" \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d @oidc.json
```

```bash
keenauth connections oidc put conn_01j9xp6q0r1s5t7v9w1x3y5z7b --data @oidc.json
keenauth connections oidc put conn_01j9xp6q0r1s5t7v9w1x3y5z7b --data @oidc-update.json --if-match '"6f1c…"'
```
