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#
Whether this org accepts passwords, single sign-on, or both.
The OIDC providers registered for this org, the redirect URI each one needs registered, and the login URL users follow.
Which group in the identity provider is worth which role in Rolter.
Every part of it requires the admin role on the org (see RBAC). A member or a viewer is told they do not have access rather than shown an empty screen.
Register an identity provider#
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, 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.
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.
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.
Fill in the rest of the provider
Back in the sheet, four fields are required:
What the button on the sign-in screen says — Acme Okta, Entra ID.
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.
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.
The application the provider registered for Rolter.
Three more are optional: Scopes (openid email profile by default), Group claim (groups by default), and Default role.
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.
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.
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.
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:
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 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.
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.
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.
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.
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.
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.
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 —
groupsunless you changed it. - Where the role applies defaults to the whole organization, which is the common case. Narrow it to grant
adminon one team, ormemberon 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. Amemberorviewermapping 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 tovieweron 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
admingrant 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 recordsmembership.last_admin_kept, and the next sign-in withdraws the grant once someone else holdsadminon the organization. Grantadminto 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).
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.
This is the same mapping model SCIM 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.
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.
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: noneor 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+statepair 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 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 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.
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. 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.