> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lyceum.technology/llms.txt
> Use this file to discover all available pages before exploring further.

# Single Sign-On (SSO)

> Let everyone at your company sign in to Lyceum through your own identity provider

## Overview

SSO lets your team sign in to Lyceum through your company's own identity provider (IdP), for example Okta, instead of a Lyceum-specific password. It's set up once per email domain by an org owner, and from then on anyone signing in from your IdP with an email at that domain is automatically added to your organization on their first login, no invite required.

SSO is configured under **Organization → SSO** in the dashboard, and is only available to the org's **owner**.

<Warning>
  Read the warnings below before you start. A couple of the choices here (removing a domain, linking an old account) are permanent and can't be undone from the dashboard once made.
</Warning>

## Before you start

<Warning>
  **Keep the org owner's password account.** Don't convert your own (the owner's) account to SSO-only. If your identity provider is ever misconfigured, or an SSO domain gets accidentally removed, an owner with no password fallback can be locked out of their own organization with no self-service way back in. Everyone else on the team can safely be SSO-only; the owner is the one account worth keeping a password on as a safety net.
</Warning>

<Warning>
  **An SSO account can never sign in with a password, even if one is set for it later.** This is a deliberate security choice. It's what makes disabling someone's account at your identity provider actually disable their Lyceum access too, rather than leaving a password-based backdoor open. If someone already had a Lyceum password account before your company turned on SSO, see [Linking an existing account](#linking-an-existing-account) below, they're a different, separate account by default.
</Warning>

<Warning>
  **Linking an old account is permanent, and safest right after your very first SSO sign-in.** If a teammate already had a Lyceum account (password-based) from before SSO was enabled, they can link it to their new SSO identity to bring over its VMs, API keys, billing history, and org role. The old account is deleted once the link completes, this can't be undone. Do it immediately after your first SSO sign-in and before using the new account for anything else: some of what carries over favors whichever account is older, so mixing real usage across both accounts before linking can leave things in a state you didn't intend.
</Warning>

<Warning>
  **Removing a domain's SSO can permanently lock people out.** Anyone who signed in via SSO loses their only way to sign in the moment the domain's SSO is removed. Their data isn't deleted, but recovering access at that point needs Lyceum support, not something they (or you) can do from the dashboard.
</Warning>

## Setting up SSO for your domain

### 1. Claim your domain

From **Organization → SSO**, enter the email domain you want to enable (e.g. `acme.com`) and choose the **default role** new SSO logins are granted: `admin` or `member`. Owner isn't an option here on purpose, granting it automatically to anyone who happens to sign in from your domain would be a serious privilege-escalation risk, so if someone needs to be an owner, promote them manually afterward from the members list.

This reserves the domain and gives you a DNS TXT record to publish.

### 2. Verify domain ownership

Publish the TXT record shown (record name and value) at your DNS provider, then click **Verify**. DNS changes can take a few minutes to propagate; if verification fails immediately after publishing, wait a bit and retry.

### 3. Create a SAML app with your identity provider

Once verified, the dashboard shows the two values your IdP needs:

* **ACS URL** (Single sign-on URL)
* **SP Entity ID** (Audience URI)

Create a new SAML 2.0 app in your IdP (in Okta: **Applications → Create App Integration → SAML 2.0**) and enter both.

<Warning>
  Also set **Name ID format** to **EmailAddress** in your IdP's app settings. Most IdPs default new apps to "Unspecified," which looks like it should work (the value is still your email) but won't.
</Warning>

Assign the users (or groups) who should have access to this app in your IdP.

### 4. Activate

Your IdP will give you SAML metadata, either a URL or an XML document, once the app is set up. Paste it back into the dashboard and click **Activate**. The domain is now `active`, and anyone signing in from that IdP with a matching email will be provisioned automatically.

## How your team signs in

Once active, there are two ways to reach SSO sign-in:

* **From your IdP directly.** Set up a bookmark/link tile in your IdP pointing at `https://dashboard.lyceum.technology/sso-login?domain=<your-domain>`. Clicking it, already authenticated with your IdP, lands the user signed into Lyceum with no further prompts.
* **From the Lyceum login page.** Click **SSO**, enter your work email, and you'll be redirected to your company's IdP to complete sign-in.

The first time someone signs in this way, Lyceum creates their account and adds them to your org with the default role you configured. Every sign-in after that just logs them back into the same account.

## Linking an existing account

If someone already has a password-based Lyceum account at the same email as their new SSO identity, they can bring its data over themselves, from **Account → Security → Link an old account**, once signed in with SSO:

1. Sign in with SSO (creates the new account, if this is the first time).
2. Go to **Account → Security → Link an old account**.
3. Enter the old account's password.
4. Confirm. VMs, API keys, billing history, and org role move onto the new SSO account. The old account is deleted.

<Note>
  We require the old account's actual password as proof of ownership, specifically so that nobody can claim someone else's account just because their IdP happens to assert the same email address.
</Note>

Do this right after the first SSO sign-in, per the warning above, before the new account accumulates any usage of its own.

## Turning off SSO for a domain

Remove the domain from **Organization → SSO**. This immediately stops new and existing SSO sign-ins for that domain. Anyone who never had a password on their account loses access entirely at that point, see the warning above before doing this.

## REST API

Base URL: `https://api.lyceum.technology/api/v2/external`. All domain endpoints require the caller to be the org's **owner**.

### SSO domains

| Method   | Endpoint                                        | Description                                        |
| -------- | ----------------------------------------------- | -------------------------------------------------- |
| `GET`    | `/orgs/{slug}/sso/domains`                      | List the org's SSO domains                         |
| `POST`   | `/orgs/{slug}/sso/domains`                      | Claim a domain; returns a DNS TXT challenge        |
| `POST`   | `/orgs/{slug}/sso/domains/{domain_id}/verify`   | Check the TXT record; `422` if not found yet       |
| `PUT`    | `/orgs/{slug}/sso/domains/{domain_id}/provider` | Register the IdP's SAML metadata and activate      |
| `DELETE` | `/orgs/{slug}/sso/domains/{domain_id}`          | Remove the domain and deregister its SAML provider |

```bash theme={null}
# Claim a domain
curl -X POST https://api.lyceum.technology/api/v2/external/orgs/acme/sso/domains \
  -H "Authorization: Bearer eyJhbGciOi..." \
  -H "Content-Type: application/json" \
  -d '{"domain": "acme.com", "default_role": "member"}'

# Activate once verified, with your IdP's metadata
curl -X PUT https://api.lyceum.technology/api/v2/external/orgs/acme/sso/domains/{domain_id}/provider \
  -H "Authorization: Bearer eyJhbGciOi..." \
  -H "Content-Type: application/json" \
  -d '{"metadata_url": "https://your-okta-tenant.okta.com/app/.../sso/saml/metadata"}'
```

A claimed domain's response includes the DNS challenge (while `pending`) and the ACS URL / SP Entity ID your IdP needs (from `verified` onward):

```json theme={null}
{
  "id": "...",
  "domain": "acme.com",
  "status": "verified",
  "default_role": "member",
  "acs_url": "https://auth.lyceum.technology/auth/v1/sso/saml/acs",
  "sp_entity_id": "https://auth.lyceum.technology/auth/v1/sso/saml/metadata",
  "verification_token": null,
  "verification_record_name": null
}
```

### Linking an account

| Method | Endpoint              | Auth                                                     |
| ------ | --------------------- | -------------------------------------------------------- |
| `POST` | `/auth/merge-account` | Authenticated (must be signed in as the new SSO account) |

```bash theme={null}
curl -X POST https://api.lyceum.technology/api/v2/external/auth/merge-account \
  -H "Authorization: Bearer eyJhbGciOi..." \
  -H "Content-Type: application/json" \
  -d '{
    "old_email": "you@acme.com",
    "old_password": "your-old-password",
    "captcha_token": "...",
    "old_mfa_code": "123456"
  }'
```

`old_email` must match the caller's own current email. `old_mfa_code` is only required if the old account has two-factor authentication enabled.
