
# OpenTofu and Terraform provider

The `keenauth/keenauth` provider manages the configuration of one KeenAuth environment as code. It calls the management API, speaks plugin protocol 6 and works with OpenTofu and Terraform. It is not published to a provider registry yet.

## Authentication and target

The provider manages the environment that its credential belongs to. To manage several environments, configure one provider alias per environment. It authenticates in exactly one of two ways: with a management API key, or keylessly as a `management` application with `private_key_jwt`. Setting both is an error.

| Setting | Environment variable | Description |
|---|---|---|
| `api_key` | `KEENAUTH_API_KEY` | Management API key. Prefer the environment variable. The key is never written to state or plan files. |
| `client_id` | `KEENAUTH_CLIENT_ID` | ID (`app_…`) of a `management` application, instead of `api_key`. Needs `private_key` and `issuer`. |
| `private_key` | `KEENAUTH_PRIVATE_KEY` | PEM PKCS#8 private key whose public key is registered on the application: EC P-256, EC P-384, Ed25519 or RSA of at least 2048 bits. Prefer the environment variable. Never written to state. |
| `private_key_algorithm` | | `PS256` (default) or `RS256`, for an RSA key only. |
| `issuer` | `KEENAUTH_ISSUER` | The environment's default host, `https://<environment>.<region>.keenauth.app`. The provider gets its token from `<issuer>/oauth2/token`. |
| `scopes` | | Permissions to request with `client_id`. If omitted, the grant's default scopes apply. |
| `region` | `KEENAUTH_REGION` | `eu` (default) or `us`. The API address is `https://api.eu.keenauth.com/v1` / `https://api.us.keenauth.com/v1`. |
| `base_url` | `KEENAUTH_BASE_URL` | Explicit API address. Only `https://<host>.keenauth.com/v1` is accepted, plus loopback addresses for local tests. |

```terraform
terraform {
  required_providers {
    keenauth = { source = "keenauth/keenauth" }
  }
}

provider "keenauth" {
  region = "eu"
}
```

### Keyless authentication with `private_key_jwt`

1. Create an application with `app_type = "management"` and a `keenauth_management_client` record with the permissions the configuration needs.
2. Register the public key of a key pair on the application (`POST /v1/applications/{application_id}/credentials` with `type: public_key`, the CLI or the dashboard).
3. Give the provider the application ID, the private key and the issuer:

```shell
export KEENAUTH_CLIENT_ID="app_..."
export KEENAUTH_PRIVATE_KEY="$(cat ops-automation.pk8.pem)"
export KEENAUTH_ISSUER="https://acme.eu.keenauth.app"
tofu plan
```

For each management token the provider signs a client assertion (RFC 7523) that is valid for 60 seconds: `iss` and `sub` are the client ID, `aud` is the issuer, `jti` is random and `kid` is the RFC 7638 thumbprint of the key. It exchanges the assertion with the `client_credentials` grant for a 10-minute token and gets a new one a minute before the old one expires. The token can never have more permissions than the application's `keenauth_management_client` record. Endpoints that accept only an API key, such as management clients, need a provider alias configured with `api_key`.

## Resources

| Name | Kind | What it manages |
|---|---|---|
| `keenauth_application` | resource | An application (OAuth 2.0 / OpenID Connect client). |
| `keenauth_connection` | resource | A login connection: database, passwordless, passkey, social or enterprise. |
| [`keenauth_connection_oidc_config`](/docs/terraform/connection-oidc-config) | resource | The relying party configuration of an enterprise OIDC connection, with a write-only client secret (OpenTofu or Terraform 1.11+). Import with the connection ID. Destroy only removes it from state. |
| [`keenauth_environment_settings`](/docs/terraform/environment-settings) | resource | Environment settings: languages, sessions, sign-up, notifications, recovery, MFA, first factors. One per environment. |
| [`keenauth_attack_protection`](/docs/terraform/attack-protection) | resource | Attack protection: brute force, IP throttling, credential stuffing, bots, breached passwords, sign-up, SMS fraud, risk policy. One per environment. |
| [`keenauth_branding`](/docs/terraform/branding) | resource | The environment theme of the branding draft. One per environment. |
| [`keenauth_custom_domain`](/docs/terraform/custom-domain) | resource | A custom login domain. |
| [`keenauth_api_key`](/docs/terraform/api-key) | resource | A management API key: import only; destroy revokes it. |
| `keenauth_organization` | resource | A B2B organization: name, slug, external ID, metadata, brand colour and membership limit. Destroy deletes it immediately; status actions (disable, enable, cancel deletion) stay in the CLI and dashboard. |
| `keenauth_organization_role` | resource | An organization-specific role and its permission set (Business plan). Import with `<org_id>/<key>`. |
| [`keenauth_permission`](/docs/terraform/permission) | resource | A permission (OAuth scope) of an API with its consent screen texts. Import with `<api_id>/<value>`. |
| [`keenauth_role`](/docs/terraform/role) | resource | An environment role and its complete permission set. User and membership assignments are not resources. |
| [`keenauth_organization_domain`](/docs/terraform/organization-domain) | resource | A domain of an organization for domain capture and SSO routing. Verification stays an action. |
| [`keenauth_session_policy`](/docs/terraform/organization-policies) | resource | An organization's session lifetimes; stricter than the environment only (Enterprise plan). |
| [`keenauth_mfa_policy`](/docs/terraform/organization-policies) | resource | An organization's MFA rules; stricter than the environment only. |
| [`keenauth_email_template`](/docs/terraform/email-template) | resource | The published content of one email template (family and locale); Startup plan. Import with `<family>/<locale>`. Destroy only removes it from state. |
| [`keenauth_management_client`](/docs/terraform/management-client) | resource | The management API permissions and IP allowlist of a `management` application. Import with the application ID. |
| [`keenauth_directory`](/docs/terraform/directory) | resource | A SCIM directory of an organization and its provisioning settings; `name` and settings change in place, `organization_id`, `scim_host` and `type` replace it. Import with the directory ID. |
| [`keenauth_directory_role_mapping`](/docs/terraform/directory-role-mapping) | resource | Maps a SCIM directory group to a role; any change replaces it. Import with `<directory_id>/<mapping_id>`. |
| [`keenauth_directory_token`](/docs/terraform/directory-token) | resource | A SCIM token of a directory, kept in state only encrypted to your RSA public key; any change creates a new token, destroy revokes it. Not importable. |
| [`keenauth_webhook_endpoint`](/docs/terraform/webhook-endpoint) | resource | A webhook endpoint; every argument changes in place. The signing secret is kept in state only encrypted to your RSA public key; changing the key or `rotation_trigger` rotates it. Import with the endpoint ID or key. |
| `keenauth_application` | data source | Reads one application by `id` or `key`. |
| `keenauth_connection` | data source | Reads one connection by `id` or `key`. |
| `keenauth_organization` | data source | Reads one organization by `id` or `slug`. |
| [`keenauth_role`](/docs/terraform/role) | data source | Reads one environment role by `id` or `key`. |
| `keenauth_environment` | data source | Reads the environment of the API key: ID, type, region, cell, state and plan. |

```terraform
resource "keenauth_connection" "google" {
  key      = "google"
  type     = "social"
  strategy = "google"
  name     = "google"
  status   = "active"
  settings = jsonencode({ scopes = ["openid", "email", "profile"] })
}

resource "keenauth_application" "storefront" {
  key            = "storefront"
  name           = "Storefront"
  app_type       = "web"
  party          = "first_party"
  redirect_uris  = ["https://shop.example.com/callback"]
  connection_ids = [keenauth_connection.google.id]
}
```

## Import

Applications and connections can be imported by their `key` or their ID, a custom domain by its hostname or its ID, and an API key by its ID:

```shell
tofu import keenauth_application.storefront storefront
tofu import keenauth_connection.google conn_01j9zq3v8c5m2x7k4t6r1n0b3d
tofu import keenauth_custom_domain.login login.example.com
```

The environment settings, the attack protection and the branding draft exist once per environment; import them with the fixed IDs `environment-settings`, `attack-protection` and `branding`. Destroying one of them only removes it from state.

## Safe changes

- A create sends an `Idempotency-Key`, so a retried create never makes a second object.
- An update or delete sends `If-Match` with the last read ETag. If someone changed the object after the plan was made, the API answers `412` and nothing changes. Run `plan` again.
- A change made in the dashboard shows up in the next `plan` as drift, and `apply` reverts it. An object deleted outside the provider is removed from state and created again.
- An update sends only the changed fields as a JSON merge patch (RFC 7396).

## Secrets

No resource stores a secret in plain text in state. Secrets that you give to KeenAuth are write-only arguments (`*_wo`): the provider sends them but never writes them to state or plan files, and sends them again only when their `*_wo_version` changes. They need OpenTofu or Terraform 1.11 or later. Today this is the client secret of an enterprise OIDC connection (`keenauth_connection_oidc_config`). Application client secrets and signing keys are not managed by this version of the provider; create and rotate them in the dashboard or with the CLI. Secrets that KeenAuth generates cannot be write-only, because a write-only argument only carries a value from the configuration to the provider. The provider never creates API keys, because the raw key would end up in state: create a key with the CLI, store it in your secret manager and import it into `keenauth_api_key` by its ID. A directory's SCIM token is shown once by the API, so the provider revokes the token that creating a directory returns and never keeps it; `keenauth_directory_token` creates the token for your identity provider and keeps it in state only encrypted to your RSA public key. `keenauth_webhook_endpoint` keeps the webhook signing secret the same way, only encrypted to its `public_key`, and discards it when no key is given. Connection `settings` must not contain secrets; the API rejects secret-like settings.

## Limits of this version

- Applications, connections and their OIDC configuration, environment settings, attack protection, the environment theme of the branding draft, custom domains, API keys (import only), organizations, roles and permissions, organization domains, organization session and MFA policies, email templates, management clients, SCIM directories, their group-to-role mappings, SCIM tokens (encrypted to your public key) and webhook endpoints are managed. Hooks, log streams and SMS templates come in later versions.
- The provider does not publish branding; publish the draft in the dashboard or with `keenauth branding publish`.
- Optional fields with a server default (for example `grant_types`) keep their last value when you remove them from the configuration.
- Schema descriptions and error messages are in English.
