# Single sign-on

> Let admins sign in through an OIDC provider.

Source: https://docs.telesphorepo.com/security/sso/

Telesphorepo can sign admins in through any OpenID Connect provider, such as Keycloak, Entra ID or Google.

SSO is for **admins**. Every SSO user is an admin. Publishers use local accounts, devices and [trust policies](/security/trust-policies/).

## How SSO accounts work

- The server creates an `oidc` account at the first SSO sign-in. It identifies the account by the pair issuer and subject.
- An SSO account has no usable password. It cannot create [read tokens](/security/read-tokens/), cannot use Basic auth, and cannot change a password.
- An SSO account has no second factor in Telesphorepo. Your identity provider handles it.
- The session lasts `oidc.session_ttl`. The default is 12 hours.
- The username comes from the `preferred_username` claim. The server lower-cases it and replaces every character outside `[a-z0-9._-]` with `-`. If a different account already owns that name, the login fails with `username_conflict`. The server never merges accounts by name.

## Admin claim rules

The server checks on **every** sign-in that the user is an admin. A user passes if either rule matches:

1. **Claim rule.** The claim named by `oidc.admin_claim` (default `groups`) contains one of the values in `oidc.admin_values`. The claim can be a string or a list. A dotted path such as `realm_access.roles` is allowed. The server reads the ID token first, then UserInfo.
2. **Email rule.** The email is verified and is in `oidc.admin_emails`.

You must set at least one of `oidc.admin_values` or `oidc.admin_emails`.

If a user fails the check, the server creates no account. If the account exists, the server deletes its sessions. When you remove a user from the admin group, access ends at the next sign-in attempt.

## Set up with Keycloak

1. In Keycloak, create a client in your realm.

   - Client type: OpenID Connect
   - Client authentication: on (a confidential client)
   - Standard flow: on
   - Valid redirect URI: `https://repo.acme.com/api/auth/oidc/callback`

2. Copy the client secret from the **Credentials** tab.

3. Create a group, for example `telesphorepo-admins`, and add your admins.

4. Add a group membership mapper to the client scope, so that the ID token carries a `groups` claim. Turn off **Full group path**, so the value is `telesphorepo-admins` and not `/telesphorepo-admins`.

5. In Telesphorepo, open **Settings**, group **sso**, and enter the values below. You can also use environment variables.

6. Test the settings, then save.

- Settings page

  | Setting            | Value                                   |
  | ------------------ | --------------------------------------- |
  | OIDC issuer URL    | `https://keycloak.acme.com/realms/acme` |
  | Client ID          | `telesphorepo`                          |
  | Client secret      | the secret from step 2                  |
  | Admin claim        | `groups`                                |
  | Admin claim values | `telesphorepo-admins`                   |

- Environment

  ```sh
  TR_BASE_URL=https://repo.acme.com
  TR_OIDC_ISSUER=https://keycloak.acme.com/realms/acme
  TR_OIDC_CLIENT_ID=telesphorepo
  TR_OIDC_CLIENT_SECRET=...
  TR_OIDC_ADMIN_CLAIM=groups
  TR_OIDC_ADMIN_VALUES=telesphorepo-admins
  ```

SSO turns on when `oidc.issuer` is set.

### Settings reference

| Setting                | Default                                | Notes                                                                     |
| ---------------------- | -------------------------------------- | ------------------------------------------------------------------------- |
| `oidc.issuer`          | none                                   | Setting it turns SSO on. Trailing slashes must match the provider exactly |
| `oidc.client_id`       | none                                   | Required                                                                  |
| `oidc.client_secret`   | none                                   | Required. Stored encrypted. The API never returns it                      |
| `oidc.redirect_url`    | `base_url` + `/api/auth/oidc/callback` | Register this URL at the provider. Either this or `base_url` must be set  |
| `oidc.scopes`          | `openid profile email`                 |                                                                           |
| `oidc.admin_claim`     | `groups`                               | Dotted paths allowed                                                      |
| `oidc.admin_values`    | none                                   | Values of the claim that make a user an admin                             |
| `oidc.admin_emails`    | none                                   | Verified emails that are admins                                           |
| `oidc.session_ttl`     | `12h`                                  |                                                                           |
| `admin_password_login` | `true`                                 | See below                                                                 |

When you save a change to the issuer, client, redirect URL or scopes, the server fetches the discovery document first. If that fails, nothing is saved and the error appears under the issuer field. The `POST /api/settings/oidc/test` route runs the same check without saving.

## Turn off password login for admins

When SSO works, you can set `admin_password_login` to `false`. Local admins can then no longer sign in to the UI with a password. Publishers with local accounts still can.

The server accepts this only while an issuer is set.

> Caution
>
> Before you turn off password login, sign in once through SSO and check that it works. If SSO breaks later, set `TR_ADMIN_PASSWORD_LOGIN=true` and restart the server. This is the break-glass path.

## Sign-in errors

A failed SSO sign-in sends the browser to `/#/login?error=<code>`.

| Code                | Cause                                                      |
| ------------------- | ---------------------------------------------------------- |
| `not_authorized`    | The user matches neither the claim rule nor the email rule |
| `username_conflict` | Another account already has this username                  |
| `invalid_username`  | The server cannot build a valid username from the token    |
| `account_disabled`  | An admin disabled the account                              |
| `sso_failed`        | Any other failure, such as a bad ID token                  |

The login route answers 404 when SSO is off, and 503 when the server cannot reach the provider.

## Gotchas

- The state of a sign-in lasts 10 minutes and works once. A reload of the callback page fails with 400. Start again from the sign-in page.
- The server refuses state from before a change of issuer or client.
- If the stored client secret cannot be decrypted (for example, after a lost master key), the server logs an error and turns SSO off at startup. Enter the secret again. Invalid `TR_OIDC_*` values stop the server from starting.
- The identity provider for **admin sign-in** is separate from the one a CI system uses for **publishing**. A trust policy has its own issuer. See [Trust policies](/security/trust-policies/).
