---
title: SSO & LDAP Authentication
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. SSO works **alongside** password authentication; it does not replace it.

SSO is available on every Sencho tier. Higher tiers add preset providers for Google, GitHub, and Okta, plus enterprise directory support via LDAP and Active Directory.

## 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 | Skipper | Preset for Google Workspace or personal Google accounts, with branded login button |
| **GitHub** | OAuth 2.0 | Skipper | Preset for GitHub personal accounts and GitHub orgs, with branded login button |
| **Okta** | OpenID Connect | Skipper | 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 enters their directory username and password on the Sencho login page.
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 (e.g., **Sign in with 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.
- **Seat limits** from your license apply. If admin seats are full, the user is downgraded to Viewer. If every seat is full, sign-in is denied with a clear error message.

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. Promotions defer cleanly when admin seats are full and apply as soon as a seat opens up.

## 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 → 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 → SSO**. 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.

<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 → Developer) 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">
    Verify the provider is **enabled** (toggle on the Active state) in **Settings → 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>
</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 flip **Require 2FA on SSO sign-in** on their **Settings → Account & Security** card to require both factors on every SSO sign-in. The toggle only appears once at least one SSO provider is enabled.
