Files
Anso e175db8e62 feat(auth): add SSO-only authentication mode (#1714)
* feat(auth): add SSO-only authentication mode

Let administrators disable interactive local password login when SSO is configured, with backend enforcement, activation safeguards, and host CLI recovery.

Closes #1709

* fix: resolve CI failures in auth mode PR

- Add useLicense mock to SSOSection test to prevent crash from
  AuthenticationModePanel rendering without LicenseProvider
- Remove username from authMode console.log calls that CodeQL flags
  as clear-text logging of sensitive information

* fix(auth): keep SSO-only on named disableSso and fail-closed login

Named provider disable no longer reverts authentication_mode. Login initializes localLoginEnabled false so a status fetch failure cannot reveal the password form. Center a single OIDC provider button on the login card.

* fix(auth): move SSO-only authentication mode from Admiral to Community tier

Security-hardening features belong on the Community tier per the existing
Community rebalance. The reporter of #1709 noted that disabling local
password login after configuring SSO is a basic security measure, not an
enterprise governance feature. LDAP provider configuration remains
Admiral-gated via requireTierForSsoProvider.

* fix(ui): keep SSO Active badge and ON toggle in sync

Provider cards mounted before config fetch finished with enabled:false, so a saved Active provider showed OFF until the local draft was resynced. Drive both the badge and TogglePill from the synced local config.

* feat(auth): auto-redirect to sole OIDC provider under SSO-only

When authentication mode is SSO only and exactly one OIDC provider is enabled (no LDAP), skip the login chooser and send the browser to that provider's authorize URL. Returning sso_error stays on the login page so the failure message remains visible.

* fix(ui): move oidcAutoRedirectUrl out of Login for fast refresh

Exporting the helper alongside the Login component tripped react-refresh/only-export-components and failed Frontend lint CI. Keep Login as a component-only module and colocate the helper with its unit tests under lib/.
2026-07-28 10:01:05 -04:00

348 lines
22 KiB
Plaintext

---
title: SSO & LDAP Authentication
sidebarTitle: SSO and LDAP
description: Authenticate with your existing identity provider, including LDAP, Google, GitHub, Okta, and any spec-compliant OIDC provider.
---
Sencho lets your team sign in with the identity provider you already use instead of maintaining a second set of credentials. By default, SSO works alongside local password authentication. You can switch to **SSO only** so interactive local password login is disabled and only configured LDAP or OIDC providers are accepted.
SSO provider configuration and authentication mode are available on every Sencho tier. Custom OIDC and the preset providers for Google, GitHub, and Okta work on every tier; LDAP and Active Directory require Sencho Admiral.
## Supported providers
| Provider | Protocol | Tier | Notes |
|----------|----------|------|-------|
| **Custom OIDC** | OpenID Connect | Community | Any spec-compliant OIDC provider: Authelia, Keycloak, Authentik, Zitadel, KanIDM, Pocket ID, and more |
| **Google** | OpenID Connect | Community | Preset for Google Workspace or personal Google accounts, with branded login button |
| **GitHub** | OAuth 2.0 | Community | Preset for GitHub personal accounts and GitHub orgs, with branded login button |
| **Okta** | OpenID Connect | Community | Preset for any Okta org or Okta-compatible IdP, with branded login button |
| **LDAP / Active Directory** | LDAP bind + search | Admiral | Works with OpenLDAP, Active Directory, FreeIPA, and any LDAPv3 server |
## How it works
### LDAP flow
1. User selects **LDAP** on the **Local / LDAP** toggle beside the **Sign in** heading and enters their directory username and password.
2. Sencho binds to LDAP with a service account, locates the user, then verifies their password.
3. On the user's first login, a Sencho account is created automatically.
4. Sencho issues a session JWT and the user is logged in, identical to a password login.
### OIDC / OAuth flow (Google, GitHub, Okta, Custom OIDC)
1. User clicks the provider button on the login page (the provider name under an **Or continue with** divider, e.g., **Google**).
2. The browser is redirected to the identity provider for authentication.
3. After granting consent, the provider redirects back to Sencho with an authorization code.
4. Sencho exchanges the code for tokens, verifies the ID token, and reads user information.
5. On the user's first login, a Sencho account is created automatically.
6. Sencho issues a session JWT and the user lands on the dashboard.
## Auto-provisioning
When a user signs in via SSO for the first time, Sencho creates a local account:
- **Username** is derived from the identity provider profile (display name, email prefix, or login handle).
- **Role** is assigned from [role mapping](#role-mapping); defaults to Viewer if no mapping matches.
- **Password** is set to an unusable placeholder. SSO users cannot sign in with the password form.
On every subsequent sign-in, the existing account is reused and the user's **email** and **role** are synced from the identity provider. Adding someone to your admin group promotes them to Admin on their next sign-in; removing them demotes them to the default role.
## Role mapping
### LDAP group mapping
Set the **Admin Group DN** to a group in your directory. Members of that group receive the Admin role; everyone else receives the default role (Viewer).
Example: if your admin group is `cn=sencho-admins,ou=groups,dc=example,dc=com`, set that as the **Admin Group DN**. Users whose `memberOf` attribute lists that DN are provisioned as Admin.
### OIDC claim mapping
For OIDC providers, configure two fields:
| Field | Description | Example |
|-------|-------------|---------|
| **Admin Claim** | The token claim name that contains role information | `groups` |
| **Admin Claim Value** | The value within that claim that grants Admin | `sencho-admins` |
If the user's ID token contains a `groups` claim with the value `sencho-admins`, they receive Admin. Otherwise, they receive the default role.
<Note>
Not every provider includes a `groups` claim by default. You may need to configure custom claims or scopes in your identity provider's admin console to surface group membership in the ID token.
</Note>
## Configuration
SSO can be configured two ways:
1. **Settings UI**: go to **Settings → Access → SSO** in the Sencho dashboard. Enable providers, paste credentials, and test connections from the UI. Changes take effect immediately, no restart required.
2. **Environment variables**: set `SSO_*` variables in your Docker Compose file. They seed the database on first boot; afterwards the Settings UI is authoritative.
### Via Settings UI
Admins manage SSO providers in **Settings → Access → SSO** (admin only). The masthead shows the SCOPE (global), the number of configured **PROVIDERS**, and how many are **ENABLED**. The page lists every provider as a collapsible card with a label, an **enable / disable** toggle pill on the right, and an **Active** badge on the header when the provider is on.
At the top of the page, **Authentication mode** chooses how interactive login works:
- **Local and SSO** (default): local username/password login remains available alongside configured providers.
- **SSO only**: local password login is disabled. The login page shows only LDAP and/or OIDC providers. Direct calls to the password login endpoint are rejected. When exactly one OIDC provider is enabled and LDAP is not, the login page redirects straight to that provider's authorization endpoint. Multiple OIDC providers still show chooser buttons. A failed SSO attempt that returns to the login page with an error stays on the page so the message is visible.
SSO only cannot be enabled until at least one provider is enabled, a connection test succeeds, and the signed-in administrator authenticated through SSO with the Admin role. Confirm the outage-risk warning before saving. Existing sessions stay valid until they expire or are revoked.
If the identity provider is unavailable after SSO only is enabled, restore local password login from the host:
```bash
docker compose exec sencho node dist/cli/enableLocalLogin.js
docker compose restart sencho
```
Restart is required so the running process reloads the setting. The same command is listed under **Settings → System → Recovery**.
<Frame>
<img src="/images/sso/sso-settings.png" alt="SSO settings panel listing the five identity providers as collapsible cards with enable / disable toggles" />
</Frame>
Click a card to expand it. The footer of every expanded form has the same actions:
- **Save**: persists changes for that provider.
- **Test Connection**: runs a live check (LDAP bind for LDAP, OIDC discovery for the OIDC providers) and shows a green check or red X next to the button with the result.
- **Remove**: clears the saved configuration (only present once a config has been saved at least once).
A static helper sits below all five cards. It reminds you that SSO users are auto-provisioned on first login and shows the OAuth callback URL template for OIDC providers:
```
https://<your-sencho-url>/api/auth/sso/oidc/<provider>/callback
```
### LDAP fields
| Field | Description |
|-------|-------------|
| **Server URL** | LDAP server URL (e.g., `ldap://ldap.example.com:389` or `ldaps://...` for TLS) |
| **Bind DN** | Service account DN used to search the directory |
| **Bind Password** | Service account password |
| **Search Base** | Base DN for user searches (e.g., `ou=users,dc=example,dc=com`) |
| **Search Filter** | LDAP filter template using `{{username}}` as a placeholder |
| **Admin Group DN** | DN of the group whose members receive the Admin role |
| **Default Role** | Role assigned to users not in the admin group (Viewer or Admin) |
| **Verify TLS certificate** | Toggle to enable or disable TLS certificate verification |
For Active Directory, set **Search Filter** to `(sAMAccountName={{username}})`. The form's helper text shows the same example inline.
<Frame>
<img src="/images/sso/sso-settings-ldap.png" alt="LDAP / Active Directory configuration form with server URL, bind DN, search base, role mapping, and Verify TLS toggle" />
</Frame>
### OIDC fields (Google, GitHub, Okta)
| Field | Description |
|-------|-------------|
| **Issuer URL** | (Okta only) Your Okta org issuer URL, e.g., `https://dev-123456.okta.com` |
| **Client ID** | OAuth client ID from your identity provider |
| **Client Secret** | OAuth client secret |
| **Admin Claim** | Token claim name inspected for role mapping (default: `groups`) |
| **Admin Claim Value** | Value within the claim that grants Admin (default: `sencho-admins`) |
| **Scopes** | Space-separated OAuth scopes (default: `openid email profile`). Customize if your provider needs additional scopes to emit group claims. |
| **Default Role** | Role assigned when no claim mapping matches (Viewer or Admin) |
Google and GitHub already know their own issuer URL, so the form omits that field for those providers.
<Frame>
<img src="/images/sso/sso-settings-oidc.png" alt="Google OIDC configuration form with client ID, client secret, admin claim mapping, scopes, and default role" />
</Frame>
### Custom OIDC fields
The **Custom OIDC** form adds the fields needed to point Sencho at a self-hosted or third-party identity provider:
| Field | Description |
|-------|-------------|
| **Display Name** | Label shown on the login button (e.g., `Corporate SSO`) |
| **Issuer URL** | Base URL of the OIDC discovery endpoint (without `/.well-known/openid-configuration`) |
| **User ID Claim** | Claim used as the unique user identifier (default: `sub`) |
| **Username Claim** | Claim used for the display name (default: `preferred_username`) |
| **Email Claim** | Claim used for the email address (default: `email`) |
Leave the three claim fields blank to use the standard OIDC defaults. Most spec-compliant providers will work out of the box; override only when your provider emits non-standard claim names.
<Frame>
<img src="/images/sso/sso-settings-custom-oidc.png" alt="Custom OIDC configuration form with display name, issuer URL, claim mapping fields, scopes, and default role" />
</Frame>
<Note>
The **User ID Claim**, **Username Claim**, and **Email Claim** fields are also accepted on Google, GitHub, and Okta as environment variables (see [Custom OIDC env vars](#custom-oidc)). The Settings UI hides them on the presets because the defaults match those providers; reach for them only if you have a custom claim layout to map.
</Note>
### Via environment variables
Environment variables are useful for initial deployment and infrastructure-as-code workflows. They seed the SSO configuration on first startup. After that, changes made in the Settings UI take precedence.
## SSO environment variables reference
### LDAP
| Variable | Default | Description |
|----------|---------|-------------|
| `SSO_LDAP_ENABLED` | `false` | Enable LDAP authentication |
| `SSO_LDAP_DISPLAY_NAME` | `LDAP` | Label shown on the login button (e.g., `Corporate AD`) |
| `SSO_LDAP_URL` | - | LDAP server URL (e.g., `ldap://ldap.example.com:389` or `ldaps://ldap.example.com:636`) |
| `SSO_LDAP_BIND_DN` | - | Service account DN for searching users |
| `SSO_LDAP_BIND_PASSWORD` | - | Service account password (encrypted at rest in the database) |
| `SSO_LDAP_SEARCH_BASE` | - | Base DN for user searches (e.g., `ou=users,dc=example,dc=com`) |
| `SSO_LDAP_SEARCH_FILTER` | `(uid={{username}})` | LDAP filter template. Use `(sAMAccountName={{username}})` for Active Directory |
| `SSO_LDAP_ADMIN_GROUP_DN` | - | DN of the group whose members receive the Admin role |
| `SSO_LDAP_DEFAULT_ROLE` | `viewer` | Role assigned to LDAP users not in the admin group |
| `SSO_LDAP_TLS_REJECT_UNAUTHORIZED` | `true` | Set to the literal string `false` to skip TLS certificate verification (useful for self-signed certs in development) |
### Google OIDC
| Variable | Default | Description |
|----------|---------|-------------|
| `SSO_OIDC_GOOGLE_ENABLED` | `false` | Enable Google SSO |
| `SSO_OIDC_GOOGLE_CLIENT_ID` | - | OAuth client ID from Google Cloud Console |
| `SSO_OIDC_GOOGLE_CLIENT_SECRET` | - | OAuth client secret (encrypted at rest) |
### GitHub OAuth
| Variable | Default | Description |
|----------|---------|-------------|
| `SSO_OIDC_GITHUB_ENABLED` | `false` | Enable GitHub SSO |
| `SSO_OIDC_GITHUB_CLIENT_ID` | - | OAuth app client ID from GitHub Developer Settings |
| `SSO_OIDC_GITHUB_CLIENT_SECRET` | - | OAuth app client secret (encrypted at rest) |
### Okta OIDC
| Variable | Default | Description |
|----------|---------|-------------|
| `SSO_OIDC_OKTA_ENABLED` | `false` | Enable Okta SSO |
| `SSO_OIDC_OKTA_ISSUER_URL` | - | Okta issuer URL (e.g., `https://dev-123456.okta.com`) |
| `SSO_OIDC_OKTA_CLIENT_ID` | - | Okta application client ID |
| `SSO_OIDC_OKTA_CLIENT_SECRET` | - | Okta client secret (encrypted at rest) |
### Custom OIDC
| Variable | Default | Description |
|----------|---------|-------------|
| `SSO_OIDC_CUSTOM_ENABLED` | `false` | Enable the custom OIDC provider |
| `SSO_OIDC_CUSTOM_DISPLAY_NAME` | `Custom OIDC` | Label shown on the login button |
| `SSO_OIDC_CUSTOM_ISSUER_URL` | - | OIDC issuer URL (the base of the discovery endpoint) |
| `SSO_OIDC_CUSTOM_CLIENT_ID` | - | OAuth client ID from your identity provider |
| `SSO_OIDC_CUSTOM_CLIENT_SECRET` | - | OAuth client secret (encrypted at rest) |
| `SSO_OIDC_CUSTOM_SCOPES` | `openid email profile` | Space-separated OAuth scopes |
| `SSO_OIDC_CUSTOM_ID_CLAIM` | `sub` | Token claim for the unique user identifier |
| `SSO_OIDC_CUSTOM_USERNAME_CLAIM` | `preferred_username` | Token claim for the display name |
| `SSO_OIDC_CUSTOM_EMAIL_CLAIM` | `email` | Token claim for the email address |
The `*_ID_CLAIM`, `*_USERNAME_CLAIM`, and `*_EMAIL_CLAIM` variables are also accepted for Google, GitHub, and Okta (substitute the provider prefix, e.g., `SSO_OIDC_OKTA_USERNAME_CLAIM`). They override the per-provider defaults when your token layout differs.
### General
| Variable | Default | Description |
|----------|---------|-------------|
| `SSO_OIDC_ADMIN_CLAIM` | `groups` | Token claim name inspected for Admin role mapping |
| `SSO_OIDC_ADMIN_CLAIM_VALUE` | `sencho-admins` | Value in the admin claim that maps to the Admin role |
| `SSO_DEFAULT_ROLE` | `viewer` | Default role for all SSO users when no mapping matches |
| `SSO_CALLBACK_URL` | auto-detect | External base URL for OAuth callback URLs (see below) |
## Reverse proxy and callback URLs
<Warning>
If Sencho is behind a reverse proxy (nginx, Traefik, Caddy), you **must** set `SSO_CALLBACK_URL` to your external URL. Otherwise, OAuth callbacks will fail.
</Warning>
Set `SSO_CALLBACK_URL` to the URL users use to reach Sencho, for example, `https://sencho.example.com`. Sencho uses this to construct the OAuth redirect URI that your identity provider calls back to.
If not set, Sencho auto-detects the URL from the request's `Host` header and protocol, which works for direct access but fails behind proxies that rewrite the host.
## Provider-specific setup examples
### Keycloak
1. Create a new client in your Keycloak realm (Client type: **OpenID Connect**).
2. Set **Valid redirect URIs** to `https://sencho.example.com/api/auth/sso/oidc/oidc_custom/callback`.
3. Enable **Client authentication** (confidential access type) and copy the client secret from the **Credentials** tab.
4. The Issuer URL is your realm URL: `https://keycloak.example.com/realms/myrealm`.
5. Keycloak uses standard claim names by default, so claim mapping can be left blank.
### Authentik
1. Create a new **OAuth2 / OpenID Provider** in Authentik.
2. Set the redirect URI to `https://sencho.example.com/api/auth/sso/oidc/oidc_custom/callback`.
3. Copy the Client ID and Client Secret.
4. The Issuer URL is `https://authentik.example.com/application/o/<slug>/`.
5. Standard claims work. For group-based admin mapping, configure a `groups` scope in Authentik so it emits the claim into the ID token.
### Authelia
1. Add an OpenID Connect client to your Authelia configuration under `identity_providers.oidc.clients`.
2. Set `redirect_uris` to include `https://sencho.example.com/api/auth/sso/oidc/oidc_custom/callback`.
3. The Issuer URL is your Authelia domain: `https://auth.example.com`.
4. Authelia uses standard OIDC claims.
### Zitadel
1. Create a new **Web** application in your Zitadel project.
2. Add `https://sencho.example.com/api/auth/sso/oidc/oidc_custom/callback` as a redirect URI.
3. The Issuer URL is your Zitadel instance URL: `https://zitadel.example.com`.
4. Copy the Client ID and Client Secret from the application settings.
### KanIDM
1. Create a new OAuth2 client in KanIDM (`kanidm system oauth2 create ...`).
2. Add the redirect URL with `kanidm system oauth2 add-redirect-url ...` and fetch the basic secret with `kanidm system oauth2 show-basic-secret ...`.
3. The Issuer URL is `https://kanidm.example.com/oauth2/openid/<client_id>`.
4. KanIDM may emit `name` instead of `preferred_username` for the username claim. If usernames look wrong after the first login, set **Username Claim** to `name`.
### Pocket ID
1. Create a new OIDC client in Pocket ID.
2. Set the callback URL to `https://sencho.example.com/api/auth/sso/oidc/oidc_custom/callback`.
3. The Issuer URL is your Pocket ID instance URL.
4. Copy the Client ID and Client Secret from the client view (Pocket ID shows the secret only at creation; reset it from the same screen if you lose it).
## Security
- **PKCE**: All OIDC flows use `code_challenge_method=S256` to prevent authorization code interception.
- **State parameter**: A cryptographic random value protects against CSRF on the OAuth callback.
- **Encrypted secrets**: LDAP bind passwords and OIDC client secrets are encrypted at rest in the Sencho database.
- **No local password**: SSO users are created with an unusable password hash and cannot bypass SSO by using the password sign-in form.
- **Admin-only configuration**: Only administrators can enable or configure SSO providers.
## Troubleshooting
<AccordionGroup>
<Accordion title="Test Connection returns 'Discovery failed' or a network timeout">
The Sencho container could not reach the provider's discovery URL. Verify the **Issuer URL** is reachable from inside the container (not just from your browser), confirm it does not include `/.well-known/openid-configuration` (just the base issuer URL), and check that container DNS can resolve the hostname. For providers with HTTPS, make sure the certificate chain is valid; self-signed certs may need additional container configuration.
</Accordion>
<Accordion title="Sign-in fails with an issuer validation error">
The `issuer` value in the provider's discovery document does not match what Sencho expects. This commonly happens when the **Issuer URL** has a trailing-slash mismatch (e.g., `https://auth.example.com` vs `https://auth.example.com/`), or when the provider is accessed via a different hostname than it advertises in its discovery document. Fix: set the **Issuer URL** to exactly match the `issuer` field returned by your provider's `/.well-known/openid-configuration` endpoint.
</Accordion>
<Accordion title="Users land with the wrong username or no email">
Enable **Developer Mode** (Settings → Operations → Developer Diagnostics) to log the raw claims Sencho receives from the provider in the server logs. Check your provider's documentation for which claims it includes in the ID token and `userinfo` response, and verify that the configured **Scopes** include everything your provider needs to emit `email` and group claims. For Custom OIDC, override **User ID Claim**, **Username Claim**, or **Email Claim** to match the names your provider actually emits.
</Accordion>
<Accordion title="The provider returns 'invalid redirect URI' during sign-in">
The callback URL registered with your identity provider must exactly match `https://sencho.example.com/api/auth/sso/oidc/<provider>/callback`, where `<provider>` is `oidc_google`, `oidc_github`, `oidc_okta`, or `oidc_custom`. If Sencho sits behind a reverse proxy, set `SSO_CALLBACK_URL` to your external URL. Some providers are strict about trailing slashes and HTTP vs HTTPS.
</Accordion>
<Accordion title="SSO buttons do not appear on the login page">
Confirm the provider is enabled (toggle on, showing the Active badge) under **Settings → Access → SSO** and that the configuration saved successfully. The login page fetches the list of enabled providers when it loads; hard-refresh the tab if changes were just made.
</Accordion>
<Accordion title="Locked out after enabling SSO only">
From the host that runs Sencho, re-enable local password login and restart so the setting takes effect:
```bash
docker compose exec sencho node dist/cli/enableLocalLogin.js
docker compose restart sencho
```
Then sign in with a local administrator account and repair the identity provider configuration before enabling SSO only again.
</Accordion>
</AccordionGroup>
The [operations troubleshooting page](/operations/troubleshooting#ldap-connection-refused) covers a few more cases that come up during initial setup, including LDAP connection refused, TLS certificate errors, and OAuth callback URL mismatches.
## Combining SSO with two-factor authentication
SSO and [two-factor authentication](/features/two-factor-authentication) work together. By default, SSO sign-ins skip the TOTP challenge, since the identity provider has already authenticated the user. Operators who want a stricter posture can turn on **Require 2FA on SSO sign-in** in **Settings → Personal → Account** to require both factors on every SSO sign-in. The toggle sits in the Two-factor authentication section and appears once you have 2FA enrolled and at least one SSO provider is enabled.