> For the complete documentation index, see [llms.txt](https://docs.drizz.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.drizz.dev/twins/self-hosting/your-own-sign-in.md).

# Your own sign-in

Let people sign in to your twins server through your own OpenID Connect provider instead of being issued keys.

Let people sign in through your own OpenID Connect provider instead of being issued keys.

|                |                                                                                          |
| -------------- | ---------------------------------------------------------------------------------------- |
| **Works with** | Okta · Microsoft Entra · Keycloak · Auth0 · Authentik                                    |
| **Runs on**    | Docker Compose · Helm chart · AWS · Google Cloud · Azure                                 |
| **You need**   | An OpenID Connect provider with signed JWT access tokens and the device sign-in          |
| **Watch out**  | The server refuses to start when the sessions setting and the provider settings disagree |

By default, a self-hosted server has sessions off. You mint every key on the server with `twins enroll` and `twins issue`.

## Prerequisites

Any OpenID Connect provider works if it:

* issues signed JWT access tokens, and
* offers the device sign-in ([RFC 8628](https://datatracker.ietf.org/doc/html/rfc8628)).

You also need a public client with the device grant on, for the console and the CLIs.

The server, `twins signin`, the `twin` CLI and the console read where the provider keeps its keys, and take a sign-in from its discovery document: `<issuer>/.well-known/openid-configuration`.

## The settings

Set `TWINS_IDENTITY_SESSIONS` to `true` and name the provider:

| Variable                  | What it is                                                                              | Example                                              |
| ------------------------- | --------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `TWINS_IDENTITY_ISSUER`   | The provider's issuer, exactly as its tokens carry it.                                  | Keycloak: `https://sso.acme.com/realms/acme`         |
| `TWINS_IDENTITY_AUDIENCE` | The audience its access tokens are issued for.                                          | Keycloak: `twins` (an audience mapper on the client) |
| `TWINS_IDENTITY_CLIENT`   | A public client with the device grant on, for the console and the CLIs.                 | Keycloak: `twins-cli`                                |
| `TWINS_IDENTITY_SCOPE`    | Scopes to ask for beside `openid`, `profile` and `email`, where the provider needs one. | Entra: `api://<app>/access`                          |
| `TWINS_IDENTITY_TRUSTED`  | Take every address the provider sends as verified, for a provider you run yourself.     | Entra sends no `email_verified`: `true`              |

The first person to sign in from a company domain owns that organization.

## Copy this

Pick your install.

{% tabs %}
{% tab title="Docker Compose" %}
`deploy/docker-compose.selfhost.yml` sets `TWINS_IDENTITY_SESSIONS: "false"` in the `worlds` service's environment. Set it to `"true"` there, and add the variables above beside it.
{% endtab %}

{% tab title="Helm chart" %}

```yaml
worlds:
  identity:
    sessions: true
    issuer: https://sso.acme.com/realms/acme
    audience: twins
    client: twins-cli
    scope: ""        # where the provider needs one (Entra: api://<app>/access)
    trusted: false   # true for a provider you run yourself (Entra sends no email_verified)
```

{% endtab %}

{% tab title="AWS" %}

```hcl
identity = {
  sessions = true
  issuer   = "https://acme.okta.com/oauth2/default"
  audience = "api://twins"
  client   = "0oa1b2c3"          # a public client with the device grant on
}
```

The object also takes `scope` and `trusted`. An identity provider outside the VPC needs a NAT gateway.
{% endtab %}

{% tab title="Google Cloud and Azure" %}
Twins is installed by the release module, so the chart's `worlds.identity` values go in the release's `values`:

```hcl
module "twins" {
  # ...
  values = [yamlencode({
    worlds = {
      identity = {
        sessions = true
        issuer   = "https://sso.acme.com/realms/acme"
        audience = "twins"
        client   = "twins-cli"
      }
    }
  })]
}
```

{% endtab %}
{% endtabs %}

## What is refused

Each install checks the settings before it serves:

| What you see                              | Fix                                                                                                      |
| ----------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| The server refuses to start, sessions on  | Name the issuer and audience.                                                                            |
| The server refuses to start, sessions off | Name none of the issuer, audience or client.                                                             |
| The Helm chart refuses the install        | Sessions on needs a provider. Sessions off needs none named.                                             |
| The AWS module fails its plan             | With sessions on, name the issuer and audience. With them off, name none of issuer, audience and client. |

## Signing in with the CLI

On a server with your own sign-in, `twin login` signs people in the usual way. It reads where your provider takes a sign-in from its discovery document. See the [CLI reference](/twins/reference/cli.md).

## Next

* [CLI reference](/twins/reference/cli.md): `twin login` against your server.
* [Keys and limits](/twins/reference/keys-and-limits.md): roles, keys and organizations.
* [Web console](/twins/using-twins/web-console.md): sign in to the console.

***

*Last updated: 27 October 2026*
