> ## 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.

# SAML Login

> Implement SAML SSO login with the Prelude JavaScript SDK.

This guide covers how to start a SAML single sign-on (SSO) login from your web application. Make sure you have [configured a SAML connection](/session/documentation/integration-guide/saml/introduction) on your backend before proceeding.

## SAML login flow

An SP-initiated SAML login involves three steps:

1. **Initiate** — Your app asks the SDK to start the flow; it redirects the user to the Identity Provider.
2. **Callback** — After authenticating, the IdP posts back and Prelude redirects to your app with a `challenge_token`.
3. **Finalize** — Your app sends the `challenge_token` to complete authentication.

The `challenge_token` is finalized exactly like [social login](/session/documentation/frontend-sdks/web/social-login) — via `finalizeOAuthLogin`.

<Accordion title="How is the SAML flow secured?" icon="shield">
  SP-initiated SAML is protected by [PKCE](https://datatracker.ietf.org/doc/html/rfc7636) (Proof Key for Code Exchange). The SDK generates a unique `code_verifier`/`code_challenge` pair for each login and stores the verifier locally. The IdP's `SAMLResponse` is matched against the `AuthnRequest` it answers (`InResponseTo`), and the `challenge_token` can only be finalized once, by the browser that started the flow.
</Accordion>

## Redirect to the Identity Provider

There are two ways to start the flow.

### By connection

When you know the connection, pass its `providerId` and `connectionId`:

```javascript theme={null}
await client.loginWithSAML({
  providerId: "okta",
  connectionId: "samlc_01jqebhswje1ka1z7ahr9rfsgt",
  redirectURI: "https://yourapp.com/callback",
});
```

### By email

To resolve the connection from the user's email domain (it must match exactly one enabled connection's allowlist), use `loginWithSAMLByEmail`:

```javascript theme={null}
await client.loginWithSAMLByEmail({
  email: "jane@acme.com",
  redirectURI: "https://yourapp.com/callback",
});
```

`redirectURI` is optional — it must be allowlisted for your app, and falls back to the connection's `default_redirect_uri` when omitted.

<Note>
  Both methods navigate the browser to the IdP. If you need the IdP URL without
  navigating (e.g. to open it yourself), call `initiateSAMLLogin` /
  `initiateSAMLLoginByEmail`, which return `{ redirect_url }`.
</Note>

## Handle the callback

When the IdP redirects back to your app, extract the `challenge_token` from the URL and finalize the login — the same call used for OAuth:

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

const params = new URLSearchParams(window.location.search);
const challengeToken = params.get("challenge_token");
const error = params.get("error");

if (error) {
  // SAML validation or provisioning failure, e.g.
  // saml_authentication_failed, saml_user_not_provisioned,
  // saml_email_domain_not_allowed.
  console.error(params.get("error_description") || error);
} else if (challengeToken) {
  try {
    await client.finalizeOAuthLogin(challengeToken);
    // The user is now authenticated. SAML challenge tokens always
    // resolve to { status: "logged_in" }.
    await client.refresh();
  } catch (err) {
    if (err instanceof PrldErrors.ExpiredChallengeToken) {
      // Challenge token has expired — restart the flow
    } else if (err instanceof PrldErrors.InvalidChallengeToken) {
      // Challenge token is invalid
    }
  }
}
```

<Accordion title="Try it" icon="flask">
  Replace `src/App.jsx` with:

  ```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 [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);
          setLoading(false);
          return;
        }

        if (challengeToken) {
          try {
            await client.finalizeOAuthLogin(challengeToken);
          } catch (err) {
            if (err instanceof PrldErrors.ExpiredChallengeToken) {
              setError("Login expired. Please try again.");
            } else if (err instanceof PrldErrors.InvalidChallengeToken) {
              setError("Invalid login. Please try again.");
            } else {
              setError("Something went wrong. Please try again.");
            }
            setLoading(false);
            return;
          }
        }

        try {
          const { user } = await client.refresh();
          setUser(user);
        } catch {
          // No active session
        }

        setLoading(false);
      };

      void init();
    }, []);

    const handleSAMLLogin = async () => {
      setError(null);
      try {
        await client.loginWithSAMLByEmail({
          email,
          redirectURI: window.location.origin + window.location.pathname,
        });
      } catch (err) {
        // The initiate endpoints reject an unknown or ambiguous email
        // domain (saml_no_connection_for_email / saml_connection_ambiguous).
        setError("No SSO connection is configured for that email domain.");
      }
    };

    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>SAML Login</h1>
          {loading ? (
            <p aria-busy="true">Completing login...</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>}
              <input
                type="email"
                placeholder="you@company.com"
                value={email}
                onChange={(e) => setEmail(e.target.value)}
              />
              <button onClick={handleSAMLLogin} disabled={!email.trim()}>
                Sign in with SSO
              </button>
            </>
          )}
        </main>
      </>
    );
  }
  ```
</Accordion>

## What's next?

If your application requires certain email domains to use SSO exclusively, see [Enforce SSO login](/session/documentation/frontend-sdks/web/saml-enforce) for handling the `saml_login_required` fallback.
