> For the complete documentation index, see [llms.txt](https://docs.slingdata.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.slingdata.io/sling-platform/platform/self-hosting/sso-mfa.md).

# SSO & MFA

Configure OIDC single sign-on and TOTP multi-factor authentication

Sling Platform supports OpenID Connect single sign-on and TOTP multi-factor authentication. Both are **self-hosted only** and require the **Advanced plan**.

## Single Sign-On (Microsoft Entra ID / OIDC)

SSO is **self-hosted only** and requires the **Advanced plan**. Set `SLING_PLATFORM_SSO` to a JSON object (or a bare provider name). When the env var is unset, login is unchanged.

`SLING_PLATFORM_HOST` **must** be the external URL browsers use. Entra ID requires an `https://` redirect URI (localhost HTTP is the only exception). A missing `SLING_PLATFORM_HOST` with SSO configured is a fatal boot error.

Register this **Web** redirect URI at the IdP:

```
https://<SLING_PLATFORM_HOST>/auth/sso/callback
```

### Microsoft Entra ID

1. Entra admin center → **App registrations** → New registration. Single tenant. Platform **Web**. Redirect URI as above.
2. Certificates & secrets → new client secret.
3. Optional (defense in depth): Token configuration → add the `email` and `xms_edov` optional claims. They are **not required**. A single-tenant app is trusted by the `tid` pin; `xms_edov` and `allowed_domains` are extra filters.

```yaml
services:
  sling-platform:
    environment:
      SLING_PLATFORM_HOST: "https://sling.example.com"
      SLING_PLATFORM_SSO: '{"provider":"microsoft","tenant_id":"<entra-tenant-id>","client_id":"<app-id>","client_secret":"<secret>","display_name":"Sign in with Microsoft","default_role":"power","allowed_domains":["company.com"],"session_duration":"12h","enforce":false}'
```

Full JSON:

<details>

<summary>SLING_PLATFORM_SSO full JSON</summary>

```json
{
  "provider": "microsoft",
  "tenant_id": "<entra-tenant-id>",
  "client_id": "<app-registration-client-id>",
  "client_secret": "<client-secret>",
  "display_name": "Sign in with Microsoft",
  "default_role": "power",
  "allowed_domains": ["company.com", "example.com"],
  "scopes": ["openid", "profile", "email"],
  "session_duration": "12h",
  "enforce": false
}
```

</details>

`tenant_id` must be a **single-tenant** ID — never `common` / `organizations` / `consumers`.

**Email trust.** First login and JIT provisioning accept the IdP email when any of these hold:

1. `provider` is `microsoft` and the ID-token `tid` matches `tenant_id` (the tenant admin is the customer).
2. `email_verified` or Entra `xms_edov` is true.
3. The email domain is in `allowed_domains`.

Otherwise the login redirects with `sso_error=email_not_verified`. For `provider: oidc`, `allowed_domains` is required and is the trust anchor when the vendor does not send `email_verified`.

JIT users are added to **every project that exists at provisioning time**. Later membership changes (remove from a project, new projects) are manual — SSO login does not re-grant access.

### Generic OIDC (Okta, Keycloak, Auth0, …)

`allowed_domains` is **required** for `provider: oidc` (it is the trust anchor when the vendor does not send `email_verified`). The issuer URL must match the `iss` claim byte-for-byte (Auth0 issuers usually end with `/`; Okta custom authorization servers use `/oauth2/<name>`).

Okta example (turn **Always include claims in token** off is fine — Sling falls back to `/userinfo`):

```json
{
  "provider": "oidc",
  "issuer_url": "https://acme.okta.com/oauth2/default",
  "client_id": "<client-id>",
  "client_secret": "<client-secret>",
  "display_name": "Sign in with Okta",
  "allowed_domains": ["acme.com"]
}
```

Keycloak example:

```json
{
  "provider": "oidc",
  "issuer_url": "https://keycloak.example.com/realms/acme",
  "client_id": "sling-platform",
  "client_secret": "<client-secret>",
  "display_name": "Sign in with Keycloak",
  "allowed_domains": ["acme.com"]
}
```

| Field                         | Required  | Default                | Notes                                                                                                                |
| ----------------------------- | --------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `provider`                    | yes       |                        | `microsoft` or `oidc`                                                                                                |
| `tenant_id`                   | microsoft |                        | Expanded to `https://login.microsoftonline.com/<tenant>/v2.0`                                                        |
| `issuer_url`                  | oidc      |                        | Full issuer URL                                                                                                      |
| `client_id` / `client_secret` | yes       |                        | From the IdP app registration                                                                                        |
| `scopes`                      | no        | `openid profile email` | `openid` is always added                                                                                             |
| `display_name`                | no        | per provider           | Login button label                                                                                                   |
| `default_role`                | no        | `power`                | Role for JIT-provisioned users (`editor` is an alias for `power`)                                                    |
| `allowed_domains`             | oidc      |                        | Email-domain allow-list. Optional for microsoft                                                                      |
| `session_duration`            | no        | PocketBase 14 days     | Go duration (`8h`, `12h`, `24h`). Applied to every session on the install                                            |
| `enforce`                     | no        | `false`                | Hide email/password and reject password auth. The `SLING_PLATFORM_ADMIN_CREDENTIALS` account is exempt (break-glass) |

{% hint style="info" %}
SSO and MFA require the **Advanced plan**. If the license is Standard (or lapsed), the platform still boots with the feature **active**, logs a warning, and shows a banner. Password login is never silently re-opened when `enforce` is on.
{% endhint %}

{% hint style="warning" %}
**Reverse proxy**: terminate TLS in front of the platform and forward `/auth/sso/*` without buffering. Entra requires HTTPS redirect URIs. The platform reads `X-Forwarded-For` / `X-Forwarded-Proto` to rate-limit by client IP (private and loopback proxies are trusted). Without these headers every user shares the proxy's IP and a team logging in together will hit 429s.

Caddy:

```
sling.example.com {
  reverse_proxy sling-platform:7878 {
    header_up X-Forwarded-For {remote_host}
    header_up X-Forwarded-Proto {scheme}
  }
}
```

nginx:

```
server {
  listen 443 ssl;
  server_name sling.example.com;
  location / {
    proxy_pass http://sling-platform:7878;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Real-IP $remote_addr;
  }
}
```

{% endhint %}

**Rate limits** (per client IP unless noted). A 429 is a real throttle, not an IdP error.

| Route                                          | Limit                                       |
| ---------------------------------------------- | ------------------------------------------- |
| `GET /auth/sso/init`, `GET /auth/sso/callback` | 60 / minute / IP                            |
| `POST /auth/sso/handoff`                       | none (one-time 32-byte code, 60s TTL)       |
| `POST /auth/login`                             | 30 / minute / IP and 10 / minute / IP+email |
| `POST /auth/login/mfa`                         | 60 / minute / IP (5 attempts per challenge) |
| MFA enroll / verify / disable / regenerate     | 20 / minute / user                          |

## Multi-Factor Authentication

MFA is **self-hosted only**, **Advanced plan**, and applies to **email/password** logins. SSO users skip Sling TOTP — enforce MFA at the IdP (Entra Conditional Access).

```yaml
# opt-in per user
SLING_PLATFORM_MFA: "true"

# require enrollment for every password user
SLING_PLATFORM_MFA: '{"required": true, "issuer": "Sling Platform"}'
```

| Field      | Default                        | Notes                                              |
| ---------- | ------------------------------ | -------------------------------------------------- |
| `enabled`  | `true` when the env var is set | Bare `"true"` is shorthand for `{"enabled": true}` |
| `required` | `false`                        | Force enrollment on next password login            |
| `issuer`   | `Sling Platform`               | Label shown in authenticator apps                  |

Users enroll from the user menu → **Security** (QR + recovery codes). Rotating the authenticator or regenerating recovery codes requires a current TOTP or recovery code. After a recovery-code login, a warning appears when **2 or fewer** codes remain.

Project admins can **Reset MFA** on the Members settings tab (shown only for enrolled users). The admin account from `SLING_PLATFORM_ADMIN_CREDENTIALS` can always use password login under SSO `enforce`; if that account enrolls MFA, the TOTP challenge still applies.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.slingdata.io/sling-platform/platform/self-hosting/sso-mfa.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
