# Single sign-on (SAML and OIDC)

> Source: https://truto.one/docs/guides/team-security/sso/

Connect your identity provider to Truto so your team signs in to `app.truto.one` with the same login they use for everything else. Then, if you want, turn off every other way in.

:::skip-reading{cli="truto sso-connections list --team-id <team_id>" skill="truto-cli"}
:::

:::callout{type="info"}
Only team admins can manage SSO. The **SSO** tab under **Settings** appears only for admins, and every API call that changes a connection or a domain returns `403` for anyone else.
:::

## How it fits together

An **SSO connection** belongs to one team and uses one protocol: **SAML 2.0** or **OIDC**. Most IdPs, including Okta, Microsoft Entra ID and Google Workspace, support at least one of them. A connection does four things:

- It signs people in through your IdP.
- It creates a Truto account the first time someone from a **verified email domain** signs in, using the connection's default role and environments.
- It can **enforce** SSO, which turns off password, magic-link, Google, password-reset and passkey sign-in for people on its verified domains.
- If it's the team's **default** connection, its default role and environments also apply to people your IdP provisions with [SCIM](/docs/guides/team-security/scim).

You set it up in five steps: create a draft, register Truto with your IdP, paste the IdP's details back and activate, verify your domain, then test. Enforcement is optional and comes last.

## 1. Create a draft connection

Truto generates the URLs your IdP needs, and the IdP generates the values Truto needs. To get past that, create the connection first as an inactive draft.

1. Open **Settings → SSO** ([app.truto.one/settings/sso](https://app.truto.one/settings/sso)).
2. Click **Add connection**.
3. Pick a **Protocol** (**OIDC** or **SAML 2.0**) and click **Continue**.

Truto creates the connection as inactive and keeps the dialog open on the configure step. The connection's card shows **Draft — not configured** until you finish it. Under **IdP configuration URLs**, the card lists the values to register with your IdP:

| Protocol | Label | Value |
|---|---|---|
| SAML | **SAML ACS URL** | `https://api.truto.one/auth/saml/<connection_id>/acs` |
| SAML | **SP Entity ID** | `https://api.truto.one/auth/saml/<connection_id>/metadata` |
| OIDC | **OIDC Callback URL** | `https://api.truto.one/auth/oidc/<connection_id>/callback` |

The SAML SP Entity ID is also the URL of Truto's SP metadata. That URL serves the metadata XML only once the connection is active. While the connection is a draft, copy the ACS URL and SP Entity ID into your IdP by hand.

```text
SAML ACS URL       https://api.truto.one/auth/saml/<connection_id>/acs
SP Entity ID       https://api.truto.one/auth/saml/<connection_id>/metadata
OIDC Callback URL  https://api.truto.one/auth/oidc/<connection_id>/callback
```

## 2. Register Truto with your identity provider

Create an application in your IdP and give it the values from step 1. The Truto values are the same whichever IdP you use. Only the IdP's screen names differ.

### SAML

- Use the **SAML ACS URL** as the IdP's Assertion Consumer Service (single sign-on, or reply) URL, and the **SP Entity ID** as its audience, or entity ID.
- Send the user's email address as the NameID. Truto asks for the `emailAddress` NameID format. If the NameID isn't an email address, Truto reads the email from an attribute named `email`, `mail`, `emailaddress`, `urn:oid:0.9.2342.19200300.100.1.3` or `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress`.
- To set the person's name, send a `displayName`, `name` or `cn` attribute, or `givenName` and `surname`. Without one, Truto uses the part of the email before the `@`.
- Sign the response or the assertion. Truto rejects unsigned responses. Truto also accepts encrypted assertions, using the certificate in its SP metadata.

### OIDC

- Register the **OIDC Callback URL** as the application's redirect URI.
- Truto uses the authorization code flow with PKCE (`S256`) and asks for the scopes `openid email profile`. It sends the client secret in the body of the token request.
- The ID token must include an `email` claim. Truto doesn't use `preferred_username`.
- The IdP must vouch for the address. It has to send `email_verified: true` in the ID token or from its userinfo endpoint, or Truto refuses the sign-in with `The identity provider has not verified this email address`.

:::callout{type="warning"}
**Microsoft Entra ID** doesn't send `email_verified`. If you connect Entra ID over OIDC, add the `xms_edov` optional claim to the ID token in your Entra app registration. Truto accepts `xms_edov: true` in its place. You can also connect Entra ID with SAML, which doesn't need the claim.
:::

From your IdP, collect the values you'll paste back in step 3:

```text
SAML:  IdP metadata XML file
       (or: IdP SSO URL, IdP entity ID, IdP X.509 signing certificate in PEM format)
OIDC:  Client ID, client secret, issuer URL (or OpenID discovery URL)
```

## 3. Paste the IdP details and activate

Go back to the dialog. If you closed it, open the connection's menu on its card and choose **Edit**.

**SAML**

- **Use IdP metadata XML** is on by default. Click **Upload IdP metadata XML** and pick the file from your IdP. Truto reads the IdP's SSO URL, entity ID and signing certificate from it, then throws the XML away.
- To enter the values yourself, turn **Use IdP metadata XML** off and fill in **IdP SSO URL**, **IdP Entity ID** and **IdP X.509 Certificate** (PEM).
- Under **Advanced**:
  - **Sign AuthnRequests** signs the sign-in requests Truto sends with Truto's SP key. Turn it on if your IdP requires signed requests. The IdP gets Truto's certificate from the SP metadata URL.
  - **Allow IdP-initiated login** accepts sign-ins that start from your IdP's dashboard or app launcher. It's **off by default**, so Truto accepts only sign-ins it started itself. Turn it on only if your IdP or your team needs it.

**OIDC**

- Fill in **Client ID**, **Client Secret** and **Issuer / Discovery URL**. Truto finds the IdP's endpoints from `<issuer>/.well-known/openid-configuration`. The client secret is write-only: Truto encrypts it and never returns it. Later edits show **Client secret is configured**, with a **Change** link.

**Both protocols**

- Under **First-login provisioning**, choose a **Default role**. For **Member**, tick at least one of the **Default environments**. Admins get every environment automatically. See [Default access for new people](#default-access-for-new-people).
- Turn on **Active**. This switch stays disabled until the required IdP fields are filled in.
- Leave **Enforce SSO** off for now. Turn it on after you've tested sign-in (step 6).
- **Set as default connection** marks the connection SCIM uses. Truto turns it on for a team's first connection.

Click **Save**.

Truto refuses to activate a connection that's missing any of these fields:

| Protocol | Required to activate |
|---|---|
| SAML | `idp_sso_url`, `idp_entity_id` and `idp_x509_cert`. Uploading IdP metadata XML fills all three. |
| OIDC | `issuer`, `client_id` and a client secret |

When a field is missing, the API returns `400` with a message like this one:

```text
Cannot activate this SAML SSO connection — missing idp_x509_cert. Save it as inactive until your identity provider is configured, then activate it.
```

## 4. Add and verify your email domain

On the connection's card, under **Email domains**, type your domain (for example `example.com`) and click **Add domain**. The domain shows **Pending verification**, and the card shows the TXT record to publish:

```text
truto-domain-verification=<verification_token>
```

Add that TXT record to the domain itself (`example.com`, not a subdomain), then click **Verify**. When the record is found, the domain changes to **Verified**. If it isn't found yet, the toast says **Domain not verified yet** and explains what was missing. DNS changes can take a while to propagate, so wait a few minutes and click **Verify** again. To check the record yourself:

```bash
dig +short TXT example.com
```

Only Truto staff can mark a domain verified without the DNS check.

### Why verification matters

A connection with no verified domain can only be reached through its direct login link. Verifying a domain changes four things:

- **Sign in with SSO works.** On the Truto login page, **Sign in with SSO** finds a connection from the domain of the work email the person types. Only verified domains on active connections match.
- **New people can join on first sign-in.** Someone with no Truto account who signs in through the connection gets one, on this team, with the default role and environments. Truto does this only for addresses on a verified domain. Anyone else is refused with `Email domain is not verified for this SSO connection`.
- **Existing users can sign in with SSO.** People who already have a Truto account, for example because you invited them and they set a password, can use SSO only if:
  - their email's domain is verified on that connection,
  - they're already a member of the connection's team (otherwise: `User is not a member of this organization`),
  - and their account is active.
- **Enforcement reaches them.** Enforcing SSO applies only to people on the connection's verified domains.

## 5. Test sign-in

On the connection's card, open the menu and choose **Test login**. Truto sends you through your IdP and back to **Settings → SSO**. For an existing admin, that works once your domain is verified (step 4).

Your team can now sign in two ways:

- On the Truto login page, enter a work email and click **Sign in with SSO**.
- Open the connection's direct login link:

```text
https://api.truto.one/auth/saml/<connection_id>/login
https://api.truto.one/auth/oidc/<connection_id>/login
```

If a sign-in fails, Truto shows the reason on its error page, for example `SAML assertion has expired (NotOnOrAfter)` or `The identity provider has not verified this email address`.

## 6. Enforce SSO

Once sign-in works, open the connection with **Edit**, turn on **Enforce SSO** and click **Save**. The card shows an **SSO enforced** badge.

Enforcement applies to everyone on one of the connection's verified domains who is a member of the team (or, for an address with no account yet, everyone on those domains). For them, Truto turns off every other way to sign in:

| Sign-in method | What they see |
|---|---|
| Password | `Password login is disabled for this organization. Please use SSO.` |
| Magic link | `Password and magic-link login are disabled for this organization. Please use SSO.` |
| Sign in with Google | `Social login is disabled for this organization. Please use SSO.` |
| Password reset | `Password login is disabled for this organization. Please use SSO.` |
| Passkey sign-in | `Passkey sign-in is disabled for this organization. Please use SSO.` |
| Registering a new passkey | `Passkeys are disabled for this organization. Please use SSO.` |

A few details decide who's covered:

- **Only active connections enforce.** Turning **Active** off on a connection also stops its enforcement.
- **Other domains are not affected.** Members whose email domain isn't verified on an active connection of the team keep their other sign-in methods. That includes guests from other companies and members whose domain you removed.
- **It follows the person, not the team.** A Truto account belongs to one person across all their teams. If any of their teams enforces SSO for their domain, they sign in with SSO everywhere.

:::callout{type="warning"}
If your IdP breaks or the connection is misconfigured, everyone on the enforced domains is locked out until enforcement is turned off. Before you enforce, make sure an admin can still sign in another way, for example an admin whose email is on a domain that isn't verified for the connection.
:::

## Default access for new people

The **First-login provisioning** settings decide what a new person gets the first time they sign in through the connection:

- **Default role: Member.** They get access to the **Default environments** you ticked. At least one is required. Without one, first sign-in fails with `Cannot JIT-provision user without default environments`.
- **Default role: Admin.** They get access to every environment of the team.

If the person belongs to SCIM groups with [group mappings](/docs/guides/team-security/scim#3-map-groups-to-roles-and-environments), the mappings decide their role and environments instead.

The team's **default** connection is the active connection marked **Set as default connection**. If none is marked, it's the oldest active connection. SCIM provisioning uses the default connection's role and environments. A team has one default at a time: marking a connection as default clears the flag on the others. Choose **Set as default** from a card's menu to switch.

Deleting a connection also deletes its domains. If the connection was enforced, make sure the people on those domains have another way to sign in.

## API and CLI

The same settings are available through the Truto API under `/sso-connection`:

| Method and path | What it does |
|---|---|
| `GET /sso-connection?team_id=<team_id>` | List connections. Filters: `protocol`, `is_active`, `enforced`, `is_default`. |
| `POST /sso-connection` | Create a connection. Requires `team_id` and `protocol`. `is_active` defaults to `true`, so send `"is_active": false` to create a draft. |
| `GET /sso-connection/<id>` | Get one connection. The client secret and SP keys are never returned. |
| `PATCH /sso-connection/<id>` | Update a connection. `config` is merged into the stored config. `config.idp_metadata_xml` is parsed into the `idp_*` fields. |
| `DELETE /sso-connection/<id>` | Delete a connection and its domains. |
| `GET /sso-connection/<id>/domains` | List the connection's domains with `verified` and `verification_token`. |
| `POST /sso-connection/<id>/domains` | Add a domain (`{"domain": "example.com"}`). The response includes `verification_record`, the exact TXT value to publish. |
| `POST /sso-connection/<id>/domains/<domain>/verify` | Run the DNS check. Returns `{"domain", "verified"}`, plus `detail` when the record wasn't found. |
| `DELETE /sso-connection/<id>/domains/<domain>` | Remove a domain. |

The connection object carries `protocol`, `is_active`, `enforced`, `is_default`, `default_role`, `default_environment_ids` and a `config` holding `idp_entity_id`, `idp_sso_url`, `idp_x509_cert`, `sign_authn_requests` and `allow_idp_initiated` (SAML) or `issuer`, `client_id` and `discovery_url` (OIDC).

:::callout{type="warning"}
Every write needs a team admin. API tokens carry no team role, so with an API token the `POST`, `PATCH` and `DELETE` calls above (verifying a domain included) return `403 Only team admins can manage SSO connections`. Make changes in the console. With an API token, you can read connections and domains.
:::

The CLI signs in with an API token, so the read commands work:

:::code-tabs

```bash label="cURL"
# List the team's SSO connections
curl "https://api.truto.one/sso-connection?team_id=<team_id>" \
  -H "Authorization: Bearer <API token>"

# List a connection's domains and their verification state
curl "https://api.truto.one/sso-connection/<connection_id>/domains" \
  -H "Authorization: Bearer <API token>"
```

```bash label="CLI"
truto sso-connections list --team-id <team_id>
truto sso-connections get <connection_id>
truto sso-connections domains <connection_id>
```

:::

The CLI also has `create`, `update`, `delete`, `add-domain`, `remove-domain` and `verify-domain`. Like the API calls, they need a team admin, so they return `403` with an API-token profile.

## Next

To create and remove team members from your IdP, and to map IdP groups to roles and environments, set up [SCIM provisioning](/docs/guides/team-security/scim). To add a second factor for people who still sign in with a password, see [Two-factor authentication and passkeys](/docs/guides/team-security/two-factor-and-passkeys).
