> ## Documentation index
> Fetch the complete documentation index at: https://rolter-ai-rolter.docs7.io/llms.txt
> Ask it with GET https://rolter-ai-rolter.docs7.io/ask?q=<keywords or question>. Short keywords rank best.
> Use these to discover all available pages before exploring further.

# Sign In to Rolter with Okta, Entra ID, or Any OIDC Identity Provider

> Register an OIDC provider from the dashboard, map its groups to Rolter roles, and decide whether your organization still accepts passwords.

Rolter can hand the dashboard's front door to your identity provider. A user picks **Continue with Acme Okta** on the sign-in screen, is bounced through the provider's authorization-code flow, and comes back with an ordinary Rolter session — the same session type a password login issues, so nothing downstream of authentication changes.

Everything below lives on one screen: **Governance → Single Sign-On**.

## What the screen manages

<CardGroup cols={3}>
  <Card title="Org sign-in policy" icon="lock">
    Whether this org accepts passwords, single sign-on, or both.
  </Card>

  <Card title="Identity providers" icon="fingerprint">
    The OIDC providers registered for this org, the redirect URI each one needs registered, and the login URL users follow.
  </Card>

  <Card title="Group mappings" icon="users">
    Which group in the identity provider is worth which role in Rolter.
  </Card>
</CardGroup>

Every part of it requires the **admin** role on the org (see [RBAC](/security/rbac)). A member or a viewer is told they do not have access rather than shown an empty screen.

## Register an identity provider

<Steps>
  <Step title="Pick a slug and copy the redirect URI">
    Open **Governance → Single Sign-On** and choose **Add provider**. Type the **Slug** first: the short name the provider will have in Rolter's URLs. The **Redirect URI** row under it fills in as you type, `https://your-rolter-host/auth/sso/<slug>/callback`, with a copy button beside it.

    A slug is lowercase letters, digits and hyphens, starts with a letter or digit, and is at most 63 characters: `okta`, `acme-entra`, `keycloak-2`. The hint under the field says so. Anything else is marked as you type, with a corrected value suggested when one can be worked out (`Acme Okta` becomes `acme-okta`), the redirect URI row says the slug is not valid instead of showing an address, and **Add provider** stays disabled. Nothing is rewritten for you: the slug is part of the redirect URI you register in your identity provider, so what you see is exactly what is saved.

    The host part is the control plane's public URL, `ROLTER_PUBLIC_URL`, as the server reports it through [`GET /api/v1/public-url`](/api/public-url), not the address your browser happens to be using. Rolter builds the redirect URI from that configured base and never from the incoming request, so a crafted link cannot send an authorization code somewhere else, and the value in the sheet is exactly the one the login flow will send.

    <Warning>
    If `ROLTER_PUBLIC_URL` is not set, the control plane falls back to `http://localhost:4001`, which reaches Rolter only from a browser on the control plane's own host. The screen says so above the provider list and beside the redirect URI. Set `ROLTER_PUBLIC_URL` to the address your users open Rolter at and restart the control plane before you register the URI with your identity provider.
    </Warning>

  </Step>

  <Step title="Create the application in your identity provider">
    Leave the sheet open and create an OIDC web application in your identity provider. Paste the redirect URI where it asks for one: Okta lists it under *Sign-in redirect URIs*, Entra ID as a *Redirect URI* on the *Web* platform, Keycloak under *Valid redirect URIs*. The identity provider then issues the client ID and, for a confidential client, the client secret.
  </Step>

  <Step title="Fill in the rest of the provider">
    Back in the sheet, four fields are required:

    <ParamField path="Name" type="string" required>
      What the button on the sign-in screen says — `Acme Okta`, `Entra ID`.
    </ParamField>

    <ParamField path="Slug" type="string" required>
      The short name in the login URL: lowercase letters, digits and hyphens, starting with a letter or digit, at most 63 characters. It cannot be changed later; registering the provider again is the only way to change it.
    </ParamField>

    <ParamField path="Issuer URL" type="string" required>
      The provider's OIDC issuer, such as `https://acme.okta.com`. Rolter reads the discovery document and the signing keys from here, and rejects an issuer that is not an `http(s)` URL.
    </ParamField>

    <ParamField path="Client ID" type="string" required>
      The application the provider registered for Rolter.
    </ParamField>

    Three more are optional: **Scopes** (`openid email profile` by default), **Group claim** (`groups` by default), and **Default role**.

  </Step>

  <Step title="Paste the client secret">
    The client secret is sealed with the deployment's key-encryption key the moment it is saved, and it is **never returned** — not on this screen, not through the API. To rotate it later, edit the provider and type the new one; you never have to delete anything. Leave the field empty for a public client that relies on PKCE alone.

    The provider card still tells you whether one is stored: the **Client secret** row reads *Stored* or *Not set*, and a provider with none carries a **No client secret** badge. That comes from `has_client_secret` on `GET /api/v1/orgs/{org_id}/sso-providers`, a boolean derived from the sealed column — the secret itself stays where it is. A provider that needs a secret and has none will fail the token exchange at login, so the badge is the warning you want before a user finds it for you.

    <Warning>
    Storing a client secret needs `ROLTER_KEK` configured on the control plane. Without it, saving a provider *with* a secret is refused — Rolter will not fall back to storing one in the clear.
    </Warning>

  </Step>

  <Step title="Check the two URLs on the card">
    The provider's card lists two addresses, each with its own copy button, and says which is which:

    - **Redirect URI**, `https://your-rolter-host/auth/sso/<slug>/callback`, is the value registered in your identity provider. It is the one to copy if you need to fix the application there.
    - **Login URL**, `https://your-rolter-host/auth/sso/<slug>/start`, is what users follow to sign in, and what the provider's button on the sign-in screen points at. Use it to test the flow or to link sign-in from an internal portal. It does not belong in the identity provider; pasting it where the redirect URI goes makes the provider refuse every sign-in with a redirect mismatch.

    Both come from the control plane as `redirect_uri` and `login_url` on each row of `GET /api/v1/orgs/{org_id}/sso-providers`, built from the same public URL, so they match what the login flow sends even behind a reverse proxy.

  </Step>
</Steps>

## Registering through the API

`POST /api/v1/orgs/{org_id}/sso-providers` takes the same fields as the sheet and applies the same slug rule. A slug outside it is refused with `400` before anything is stored, and the message states the rule:

```json
{
  "error": {
    "message": "config error: slug must be lowercase letters, digits and hyphens, start with a letter or digit, and be at most 63 characters"
  }
}
```

The slug is checked exactly as sent. It is not trimmed or lowercased, so `Okta` and `okta ` are both refused rather than quietly turned into something else. An empty slug is a `400` too, with its own message, `slug must not be empty`.

## Editing a provider

A registered provider is editable in place — name, issuer, client ID, scopes,
group claim, default role, and the client secret. Use the pencil on the
provider's card, or `PUT /api/v1/sso-providers/{id}`.

Three things are worth knowing about the edit:

- **The slug is fixed.** It is in the login URL (`/auth/sso/<slug>/start`), which
  is already in bookmarks, IdP redirect allowlists and runbooks. A provider that
  needs a different slug is a different provider.
- **An empty client secret field means "keep".** The stored secret is not
  readable, so the form cannot show it — leaving the field alone leaves the
  sealed secret exactly where it is. Whitespace counts as empty: a stray space
  or a pasted newline changes nothing. Type a new value to rotate it. Saving the
  form can never remove a secret — see
  [Removing a stored client secret](#removing-a-stored-client-secret) for the
  control that does.
- **Group mappings survive.** They hang off the provider's id, and editing keeps
  that id, so the roles your IdP groups grant are untouched.

<Note>
  Every edit is written to the audit log as `sso_provider.update`, recording
  whether the client secret was left unchanged, rotated, or cleared — never the
  secret itself.
</Note>

## Removing a stored client secret

Turning a provider into a public client — one that relies on PKCE alone — is its
own action, separate from editing. On the provider's card, the eraser button
next to the pencil reads **Remove stored secret**, and it only appears while
there is one to remove. It names the provider in a confirmation before anything
leaves the browser, because a sealed secret cannot be read back: if you remove
the wrong one, the only way forward is to fetch a fresh secret from your identity
provider.

Confirming sends `client_secret: ""` to `PUT /api/v1/sso-providers/{id}` — the
wire's third value, distinct from omitting the field (keep) and from sending one
(rotate). The card's **Client secret** row then reads _Not set_ and the
**No client secret** badge appears.

<Warning>
  If your identity provider still expects a client secret, every sign-in through
  the provider fails at the token exchange from the moment the secret is
  removed. Remove one only when you have deliberately switched the IdP
  application to a public client.
</Warning>

## Taking a provider out of service

The switch on the provider card flips `enabled`. A disabled provider disappears
from the login screen and from `GET /api/v1/auth/methods`, and its `/start` URL
stops resolving — but the registration, its secret and its group mappings all
stay. Flip it back and sign-in resumes.

Switching a provider off asks first, because the people signing in through it
lose that route immediately. Switching it back on sends at once. The switch and
the pencil beside it take the same admin role as the rest of the card, and are
disabled, naming the role they need, for anyone without it.

This is what you want during an IdP migration or while a broken secret is being
chased down. Deleting the provider would also stop the logins, and would take
the group mappings with it.

<Warning>
If password sign-in is off for the org and this is its only enabled provider, the control plane refuses to disable it and answers `409 Conflict`: no member could sign in afterwards. Only superadmins could, because the password setting does not apply to them. Enable another provider or turn password sign-in back on first. With another provider enabled, or with password sign-in on, the change goes through.
</Warning>

## Map groups to roles

A group arriving from your identity provider grants nothing on its own. **You** decide what it is worth, in the **Group mappings** panel on the provider's card: type the group name exactly as the IdP publishes it, pick where the role applies — the whole organization, a team, or a project — pick `admin`, `member` or `viewer`, and choose **Map group**.

- Mappings are matched against the claim named in **Group claim** — `groups` unless you changed it.
- **Where the role applies** defaults to the whole organization, which is the common case. Narrow it to grant `admin` on one team, or `member` on a single project, without giving the group anything org-wide. The scope list covers the whole organization: every team, and every project in any of those teams grouped under the team that owns it, so a mapping onto a project outside the team the scope switcher currently shows needs no detour through the switcher. An organization with no teams yet says so, and offers only itself.
- **Role to grant** starts on `viewer`, the least powerful role, so pressing **Map group** without choosing one never gives a group more than viewer access. The fields are labelled, and the group's placeholder is marked as an example (`e.g. platform-engineering`) so an empty field is not mistaken for a filled one.
- A mapping that grants `admin`, or grants at the whole organization, is confirmed before it is written. The dialog names the group, the role and the scope, says that everyone in that group at your identity provider gets the role at their next sign-in, and lists what raised it: the role, the scope, or both. A `member` or `viewer` mapping on one team or one project saves without asking. The scope starts on the whole organization, so a mapping left at its defaults is confirmed too. Once a mapping is written, the form goes back to `viewer` on the whole organization, so the next mapping does not inherit the last grant.
- Every mapping grants inside the provider's own org. A mapping can never reach another tenant, whatever the identity provider sends or what you pick here.
- Each mapping is listed with the scope it grants at, so a team-scoped one is never mistaken for an org-wide one. A scope the dashboard cannot name is marked **Unresolved scope** instead of being drawn as a bare id; the id stays in the chip's tooltip, and the removal prompt names it inline.
- Removing a mapping stops that group being granted the role — at that scope — on the next sign-in. The prompt names both before anything is withdrawn, and roles someone already holds are not taken back.
- **The last admin stays.** When a sign-in would withdraw an `admin` grant at the whole organization from the last active account holding one, Rolter keeps that grant instead. The sign-in itself is not refused for it, the audit log records `membership.last_admin_kept`, and the next sign-in withdraws the grant once someone else holds `admin` on the organization. Grant `admin` to the next person first, then change the group. This holds when a superadmin removes the mapping too, since the change only takes effect at the next sign-in; a superadmin who means to remove the last admin can revoke that grant directly from **Governance → Users** ([managing roles](/security/rbac#managing-roles-on-the-users-screen)).

<Warning>
A user who matches no mapping falls back to the provider's **Default role**. Leave that unset and they are **refused** instead of quietly admitted with no memberships — which is the safe default, and the reason a provider with neither a mapping nor a default role cannot log anybody in.
</Warning>

This is the same mapping model [SCIM provisioning](/security/user-provisioning) uses, so you only learn it once.

## Choose how the org signs in

The **Org sign-in policy** card at the top of the screen carries two switches, and they are saved together.

| Password sign-in | Single sign-on | What the sign-in screen shows                                     |
| ---------------- | -------------- | ----------------------------------------------------------------- |
| on               | on             | the email and password form, plus one button per enabled provider |
| off              | on             | only the provider buttons                                         |
| on               | off            | only the email and password form                                  |

A provider whose org has single sign-on off is left out of the sign-in screen and of `GET /api/v1/auth/methods`, while another org's providers stay listed. Its `/start` URL refuses before the identity provider is involved: a browser comes back to the sign-in screen with the reason, and any other caller gets a `403` whose `error.code` is `sso_disabled`.

Two combinations are refused, with the reason shown inline:

- **Both off** is an outage rather than a policy, so the dashboard will not send it.
- **Password sign-in off with no enabled provider** would lock every non-superadmin out. Register the provider first, then turn passwords off.

Turning password sign-in **off** confirms before it saves. From then on every member except a superadmin signs in through an identity provider, and the confirmation says so. It also lists every enabled provider that has no client secret stored. That is a warning rather than a refusal, because a provider that relies on PKCE alone has no secret on purpose. If your identity provider does expect one, sign-ins through that provider fail at the token exchange, and when every enabled provider is on the list no member could complete a sign-in until a secret is stored. Turning password sign-in on, or saving another field while it is already off, asks nothing.

Turning single sign-on **off** confirms too, when the org has at least one enabled provider. While the switch is off, the sign-in screen stops offering the org's providers, and the control plane refuses every sign-in through one of them, whether it is just starting or was begun before the switch and is coming back from the identity provider. An account created by signing in through a provider has no password, so those members cannot sign in until single sign-on is back on or a superadmin sets a password for them with `PUT /api/v1/users/{id}`. Accounts that do have a password, such as those created from an invitation, keep signing in with it. The providers, their secrets and their group mappings stay, so turning single sign-on back on restores the buttons and sign-in. The confirmation says all of this and does not block the change. With no enabled provider nobody is signing in through one, so the save goes straight out, and turning single sign-on on asks nothing.

<Info>
The superadmin is exempt from `Password sign-in: off` on purpose. A mistyped issuer or an outage at the identity provider would otherwise leave nobody able to sign in and fix it. That exemption is what makes the switch safe to use.
</Info>

## What a user sees when signing in

Choosing **Continue with …** on the sign-in screen takes the user to the identity provider and back, and they land signed in on the dashboard. The session token never appears in the address bar: the provider's redirect carries a one-time code that is good for a minute and works once, and the dashboard swaps it for the session straight away.

If the sign-in is refused, the user comes back to the sign-in screen with the reason. The reasons are: the provider declined the sign-in, the sign-in took too long or was already used, SSO is turned off for the organization, the user is in no mapped group, the account is deactivated, the account belongs to another organization, to a superadmin or to a holder of a deployment role, or the provider's answer could not be verified. The provider's own error text is never shown.

Scripts and other non-browser clients that call the callback directly (without asking for `text/html`) still receive the session as JSON.

## What the login flow refuses to do

- **No unsigned trust.** The id token is verified against the JWKS fetched from the issuer's discovery document, with the issuer, audience and nonce all checked. An `alg: none` or HMAC-signed token is rejected — only the asymmetric algorithms the provider publishes are accepted.
- **No open redirect.** The redirect URI comes from the deployment's own base URL, never from the request.
- **One-shot state.** The login state row is consumed by the callback and expires, so a replayed `code`+`state` pair finds nothing to redeem.
- **One-shot sign-in code.** A browser is handed a code, not the session. The code expires after a minute, works once, and only its digest is stored.
- **No metadata addresses.** The issuer is checked against the [egress policy](/configuration/egress) when the provider is saved, and the discovery document, token endpoint and JWKS are checked again each time they are fetched. These calls do not follow redirects.
- **No implicit access.** A user in no mapped group gets the provider's default role, or is refused when there is none.
- **No adoption across organizations.** A provider signs in as an existing account with the same email only when every role that account holds is in the provider's own organization. It never signs in as a superadmin, or as an account that holds a deployment role (operator or security auditor), even one with no organization roles. Whoever registers a provider decides which emails it asserts, so anything wider would let one organization's admins sign in as another organization's members. A person with roles in two organizations signs in with their password instead.

## Removing a provider

Deleting a provider is confirmed by name first, because it takes a whole route in away at once: anyone signing in through it loses it immediately, and its group mappings go with it. If you only need it to stop accepting logins for a while, [disable it](#taking-a-provider-out-of-service) instead — that keeps the mappings and the sealed secret. Accounts it already created stay, with the roles they already hold — deletion is not a way to lock people out. Deactivate those accounts from **Governance → Users** if that is what you meant.

<Warning>
If the org's policy has password sign-in turned off, deleting its only enabled provider is refused with `409 Conflict`, for the same reason as [disabling it](#taking-a-provider-out-of-service). Turn password sign-in back on first, or register the replacement provider before you delete this one. A provider that is already disabled can be deleted freely.
</Warning>
