# Single sign-on (SSO)

An organization can connect its own identity provider (IdP) so members sign in
to the console through it. Scribase supports **OpenID Connect** (Okta, Microsoft
Entra ID, Google Workspace, Auth0, Keycloak, and any other OIDC provider) and
**SAML 2.0**. Owners and admins configure it in the console under
**Single sign-on**, or through the [API](#api).

## How sign-in works

1. On the sign-in page, a person chooses **Sign in with your company (SSO)** and
   enters their work email.
2. The email's domain must be verified by an organization with SSO enabled.
   The browser goes to that organization's IdP.
3. The IdP sends the browser back to the control API, which validates the
   response, then back to the console, which receives a session bound to that
   organization.

Who the person becomes in Scribase:

- **A linked account.** An administrator who runs the connection test links
  their IdP identity to their existing account and keeps their role.
- **An invitation.** If there is a pending invitation for the email address,
  signing in through the IdP accepts it with the invited role.
- **An automatic join.** If the email is on a verified domain and automatic
  join is on, the person joins with the chosen role (developer or viewer).
- Anyone else is refused with a message to ask an administrator for an
  invitation.

A session from SSO lasts 12 hours by default and only works for the
organization whose IdP issued it.

## Set up OIDC

1. At your IdP, create a web application (confidential client) and register
   the **redirect URI** shown in the console:
   `https://<api>/v1/sso/oidc/callback`.
2. Allow the `openid`, `email`, and `profile` scopes.
3. In the console, choose **OpenID Connect** and enter the issuer URL, client ID,
   and client secret. Scribase checks the issuer's discovery document when you
   save. The client secret is stored encrypted and never shown again.

The sign-in uses the authorization-code flow with PKCE and a nonce. The ID
token's signature is verified against the provider's published keys (RS256,
PS256, ES256, and EdDSA are accepted), along with its issuer, audience, expiry,
and nonce. An email the provider marks as unverified is ignored.

## Set up SAML 2.0

1. At your IdP, create a SAML application with:
   - **ACS URL** (HTTP-POST): `https://<api>/v1/sso/saml/<org>/acs`
   - **Entity ID / audience**: `https://<api>/v1/sso/saml/<org>/metadata`
     (this URL also serves the SP metadata document)
   - **Name ID**: the user's email address (or send an `email` attribute)
   - **Signing**: sign the assertion or the response with RSA-SHA256
   - **Encryption**: off
2. In the console, choose **SAML 2.0** and paste the IdP metadata XML, or enter
   the IdP entity ID, its single sign-on URL (HTTP-Redirect binding), and its
   signing certificate.

Every response is checked before anything in it is used: the XML signature
against your certificate (exclusive canonicalization, SHA-256 or SHA-512; SHA-1
is refused), exactly one assertion, the issuer, the audience, the recipient,
the `InResponseTo` link to the sign-in Scribase started, and the validity
window. Sign-ins started at the IdP (IdP-initiated SSO) are not supported:
start from the Scribase sign-in page.

## Verify your domains

Add each email domain your people use, then publish the TXT record the console
shows:

| Name | Type | Value |
|---|---|---|
| `_scribase-sso.example.com` | TXT | `scribase-sso-verification=<token>` |

Choose **Verify** once the record is live. A domain can be verified by one
organization. Only verified domains route sign-ins to your IdP and allow
automatic joins.

## Test, then require SSO

**Test connection** signs you in at the IdP without changing your session. A
passing test links your IdP identity to your account and records that these
exact settings work. Changing the IdP settings later clears the result.

**Require SSO** can only be turned on after a passing test against the current
settings. While it is on:

- Everyone except **owners** must use an SSO session for this organization.
  Owners are exempt so a broken IdP can never lock the organization out.
- Personal access tokens stop working in this organization for non-owners.
  Organization API keys keep working; use them for CI and automation.

To change the IdP while SSO is required, save the new settings with the
requirement off, run the test, and turn it back on.

## API

All configuration routes need an owner or admin (reading needs any member).

| Route | Purpose |
|---|---|
| `GET /v1/organizations/{org}/sso` | Connection, domains, and the values to register at the IdP |
| `PUT /v1/organizations/{org}/sso` | Create or replace the connection |
| `DELETE /v1/organizations/{org}/sso` | Remove it and end all SSO sessions |
| `POST /v1/organizations/{org}/sso/domains` | Claim a domain |
| `POST /v1/organizations/{org}/sso/domains/{domain}/verify` | Check its TXT record |
| `DELETE /v1/organizations/{org}/sso/domains/{domain}` | Release it |
| `POST /v1/organizations/{org}/sso/test` | Start a connection test |
| `POST /v1/sso/start` | Begin sign-in for an email |
| `POST /v1/sso/exchange` | Redeem the one-time code for a session or test result |
| `POST /v1/sso/logout` | End an SSO session |

Configuration changes, domain verification, sign-ins, and automatic joins are
written to the organization's [audit log](https://docs.scribase.com/docs/api/audit.md).

Self-hosting needs `SCRIBASE_PUBLIC_API_URL` and a few optional settings; see
[Self-hosting](https://docs.scribase.com/docs/self-hosting.md#organization-single-sign-on-optional).
