mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-26 11:49:16 +00:00
ce08a593d7
* refactor(settings): split System Limits and regroup the hub System Limits had grown into a grab-bag of host alert thresholds, Docker cleanup, and mesh data-plane controls under one mislabeled section. Split it into Host Alerts, Docker & Storage, and Fleet Mesh, and split Developer into Developer Diagnostics and Data Retention. Reorganize the sidebar into ten domain groups: Personal, Access, Infrastructure, Monitoring, Notifications, Automation, Organization, Security, Operations, Help. Each section now saves only its own keys, so a concurrent edit in one section no longer clobbers another. Data Retention sends the audit-log window only on a paid plan, matching the field's existing visibility, so a Community save no longer fails on a key the operator cannot set. NumberChip moves to a shared module and the toggle reuses the existing shared component. The /settings API is unchanged. * test(settings): cover registry structure and per-section save payloads Add structural invariants for the ten-group registry (every item maps to a real group, ids are unique, the System Limits and Developer splits land in the right groups with the right gates, renamed labels and the Registries paid gate hold) and per-section payload tests asserting each split section patches only its own keys, including the Community path where Data Retention omits the paid audit-log key. * docs(settings): document the regrouped settings hub Rewrite the settings reference for the ten-group layout, replace the System Limits page with Host Alerts, Docker & Storage, and Fleet Mesh, and document the prune-on-update, reclaimable-space banner, and mesh auto-recreate settings that were previously undocumented. Update the Settings navigation breadcrumbs across the feature docs and refresh the affected screenshots. * fix(settings): show Access sections as instance-global, not operator-scoped License, Users, SSO, and API Tokens are instance-global settings but the masthead scope label rendered them as operator-scoped because it keyed off the old Identity group. Only Personal sections (account, appearance) are operator/browser-scoped now; everything else reads as global. Also add a compile-time exhaustiveness guard to the section switch so a future SectionId added without a matching case fails the build instead of silently rendering a blank panel. * docs(settings): remap remaining settings breadcrumbs to the new groups Update the navigation breadcrumbs that still pointed at the removed Identity, Alerts, and Advanced groups: API Tokens and Users now sit under Access, Webhooks under Automation, Labels under Organization, App Store under Infrastructure, Appearance under Personal, and scan policies under Security > Vulnerability Scanning. Correct the settings reference scope note so Access reads as global. * docs(settings): remap renamed-section breadcrumbs across feature docs Sweep every feature, operations, getting-started, and reference page for navigation paths that still named the renamed settings sections, and point them at the current ones: Security becomes Security > Vulnerability Scanning, Notifications becomes Notifications > Channels, Routing becomes Notifications > Notification Routing, and Developer becomes Operations > Developer Diagnostics (with its retention windows under Operations > Data Retention). App Store moves under Infrastructure and the four-group overview in the getting-started intro is rewritten to the ten groups. Separators each page already used are preserved.
321 lines
19 KiB
Plaintext
321 lines
19 KiB
Plaintext
---
|
|
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. 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 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.
|
|
|
|
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 → 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 → 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">
|
|
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.
|