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

# Enforce SSO Login

> Transparently fall back to SAML when a domain enforces SSO, with the Prelude JavaScript SDK.

When a SAML connection has [enforce login](/session/documentation/integration-guide/saml/enforce) enabled, users whose email domain is covered by the connection must authenticate through SSO. Other login methods are refused — and the Web SDK gives you a typed signal so you can route those users into SAML without showing them an error.

## The `saml_login_required` error

If your app starts an OTP login for an enforced email, the server responds with `403 saml_login_required`. `startOTP` routes failures through the SDK's error mapper, so it throws a typed `SAMLLoginRequiredError`, exported as `PrldErrors.SAMLLoginRequired`:

```javascript theme={null}
import { PrldErrors } from "@prelude.so/js-sdk";

try {
  await client.startOTP({
    identifier: { type: "email_address", value: email },
  });
  // OTP sent — show your code-entry screen.
} catch (err) {
  if (err instanceof PrldErrors.SAMLLoginRequired) {
    // This domain enforces SSO — restart via SAML instead.
  } else {
    // Handle other errors (rate limiting, invalid identifier, …).
  }
}
```

## Fall back to SAML

On `SAMLLoginRequiredError`, restart the flow with `loginWithSAMLByEmail`. It resolves the connection from the same email domain and redirects the user to the Identity Provider:

```javascript theme={null}
import { PrldErrors } from "@prelude.so/js-sdk";

async function startEmailLogin(email) {
  try {
    await client.startOTP({
      identifier: { type: "email_address", value: email },
    });
    // OTP path: show the code-entry screen.
  } catch (err) {
    if (err instanceof PrldErrors.SAMLLoginRequired) {
      // Enforced domain — hand off to SSO. This navigates to the IdP.
      await client.loginWithSAMLByEmail({
        email,
        redirectURI: window.location.origin + window.location.pathname,
      });
      return;
    }
    throw err;
  }
}
```

The user authenticates with the IdP and is redirected back with a `challenge_token`, which you finalize exactly as in the [SAML Login](/session/documentation/frontend-sdks/web/saml#handle-the-callback) guide.

<Note>
  The fallback is transparent: the user enters their email expecting an OTP and
  is seamlessly redirected to their company's SSO instead. No separate "Sign in
  with SSO" button is required.
</Note>

<Accordion title="Try it" icon="flask">
  This single email field starts an OTP login and silently upgrades to SAML when the domain enforces SSO.

  ```jsx src/App.jsx theme={null}
  import "@picocss/pico";
  import { useState, useEffect } from "react";
  import { PrldSessionClient, PrldErrors, decode } from "@prelude.so/js-sdk";

  const client = new PrldSessionClient({ domain: `${import.meta.env.VITE_APP_ID}.session.prelude.dev` });

  export default function App() {
    const [user, setUser] = useState(null);
    const [email, setEmail] = useState("");
    const [code, setCode] = useState("");
    const [otpSent, setOtpSent] = useState(false);
    const [error, setError] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
      const init = async () => {
        const params = new URLSearchParams(window.location.search);
        const challengeToken = params.get("challenge_token");
        const callbackError = params.get("error");

        if (challengeToken || callbackError) {
          window.history.replaceState({}, "", window.location.pathname);
        }

        if (callbackError) {
          setError(params.get("error_description") || callbackError);
        } else if (challengeToken) {
          // Returning from the IdP — finalize the SAML login.
          try {
            await client.finalizeOAuthLogin(challengeToken);
          } catch {
            setError("SSO login failed. Please try again.");
          }
        }

        try {
          const { user } = await client.refresh();
          setUser(user);
        } catch {
          // No active session
        }
        setLoading(false);
      };
      void init();
    }, []);

    const handleEmail = async () => {
      setError(null);
      try {
        await client.startOTP({ identifier: { type: "email_address", value: email } });
        setOtpSent(true);
      } catch (err) {
        if (err instanceof PrldErrors.SAMLLoginRequired) {
          // Enforced domain — redirect to the IdP.
          await client.loginWithSAMLByEmail({
            email,
            redirectURI: window.location.origin + window.location.pathname,
          });
        } else {
          setError("Could not start login. Please try again.");
        }
      }
    };

    const handleCode = async () => {
      setError(null);
      try {
        await client.checkOTP({ code });
        const { user } = await client.refresh();
        setUser(user);
      } catch {
        setError("Invalid code. Please try again.");
      }
    };

    return (
      <>
        <style>{`body, #root { display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; padding: 0; }`}</style>
        <main style={{ textAlign: "center", maxWidth: "600px", width: "100%" }}>
          <h1>Sign in</h1>
          {loading ? (
            <p aria-busy="true">Loading...</p>
          ) : user ? (
            <article>
              <p>Logged in</p>
              <pre style={{ textAlign: "left", whiteSpace: "pre-wrap", wordBreak: "break-all" }}>
                {JSON.stringify(decode(user.accessToken).claims, null, 2)}
              </pre>
              <button className="secondary" onClick={async () => { await client.logout(); setUser(null); }}>
                Log Out
              </button>
            </article>
          ) : (
            <>
              {error && <p role="alert" style={{ color: "var(--pico-color-red-500)" }}>{error}</p>}
              {!otpSent ? (
                <>
                  <input type="email" placeholder="you@company.com" value={email} onChange={(e) => setEmail(e.target.value)} />
                  <button onClick={handleEmail} disabled={!email.trim()}>Continue</button>
                </>
              ) : (
                <>
                  <input inputMode="numeric" placeholder="123456" value={code} onChange={(e) => setCode(e.target.value)} />
                  <button onClick={handleCode} disabled={!code.trim()}>Verify</button>
                </>
              )}
            </>
          )}
        </main>
      </>
    );
  }
  ```
</Accordion>

## What's next?

Read the [Enforce SSO login](/session/documentation/integration-guide/saml/enforce) integration guide for the backend configuration, or the [SAML Login](/session/documentation/frontend-sdks/web/saml) guide for the standard flow.
