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

# Social Login

> Implement social login with the Prelude JavaScript SDK.

This guide covers how to implement social login (OAuth) in your web application. Make sure you have [configured a social login provider](/session/documentation/integration-guide/social-login) on your backend before proceeding.

## OAuth login flow

The social login flow involves three steps:

1. **Redirect** — Your app redirects the user to the provider's login page
2. **Callback** — The provider redirects back to your app with a `challenge_token`
3. **Finalize** — Your app sends the `challenge_token` to complete authentication

When the OAuth provider has [`verify_email`](/session/documentation/integration-guide/social-login#verify-email-via-otp) enabled and the IdP returns an email it has not verified, an extra OTP step happens between **Callback** and **Finalize** — see [Verify email via OTP](#verify-email-via-otp) below.

<Accordion title="How is the OAuth flow secured?" icon="shield">
  The entire flow is protected by [PKCE](https://datatracker.ietf.org/doc/html/rfc7636) (Proof Key for Code Exchange). The SDK generates a unique `code_verifier` and `code_challenge` pair for each login attempt, and the `challenge_token` can only be finalized once. This protects against:

  * **Authorization code interception** — A stolen `challenge_token` is useless without the `code_verifier`, which never leaves the browser
  * **Replay attacks** — The `challenge_token` is invalidated after a single use
  * **Cross-site request forgery (CSRF)** — The server validates that the finalize request matches the original authorization request
</Accordion>

## Redirect to the provider

Use `loginWithOAuth` to redirect the user to the provider's authorization page:

```javascript theme={null}
await client.loginWithOAuth({
  provider: "google",
  redirectURI: "https://yourapp.com/callback",
});
```

The `redirectURI` must match the redirect URI configured in your OAuth provider settings.

## Handle the callback

When the provider redirects back to your app, extract the `challenge_token` from the URL and finalize the login:

```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) {
  // The provider returned an error (user denied access, etc.)
  console.error(params.get("error_description") || error);
} else if (challengeToken) {
  try {
    const result = await client.finalizeOAuthLogin(challengeToken);
    if (result.status === "logged_in") {
      // User is now authenticated
    } else if (result.status === "otp_required") {
      // Provider returned an unverified email and the app has
      // verify_email enabled. The OTP has already been sent —
      // route to your OTP screen and call checkOTP with the code.
      // See the "Verify email via OTP" section below.
    }
  } catch (error) {
    if (error instanceof PrldErrors.ExpiredChallengeToken) {
      // Challenge token has expired — restart the flow
    } else if (error instanceof PrldErrors.InvalidChallengeToken) {
      // Challenge token is invalid
    } else if (error instanceof PrldErrors.BadRequest) {
      // Invalid request
    } else if (error instanceof PrldErrors.RateLimited) {
      // Too many attempts
    }
  }
}
```

## Verify email via OTP

When the OAuth provider config has [`verify_email`](/session/documentation/integration-guide/social-login#verify-email-via-otp) enabled and the IdP returns an unverified email, Session does not finalize the login on the callback. Instead, the redirect carries `status=otp_required` alongside the `challenge_token`, and `finalizeOAuthLogin` returns:

```typescript theme={null}
{ status: "otp_required"; challengeId: string; email: string }
```

The OTP has already been sent to `email`. Your app shows an OTP screen, the user enters the code, and you call `checkOTP`:

```javascript theme={null}
const result = await client.finalizeOAuthLogin(challengeToken);

if (result.status === "otp_required") {
  // result.email is the address the OTP was sent to
  // Show your OTP screen, then call:
  await client.checkOTP({ code: codeFromUser });
  // checkOTP verifies the OTP against the verification cookie
  // and finalizes the login internally — no challengeId needed.

  // Optional: resend the OTP
  // await client.retryOTP();
}
```

After `checkOTP` resolves successfully the user is fully logged in — call `client.refresh()` (or whatever your app uses to load the session) and proceed.

<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 [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 providers = ["google", "apple", "github", "microsoft", "okta", "facebook"];

    const handleLogin = async (provider) => {
      setError(null);
      try {
        await client.loginWithOAuth({
          provider,
          redirectURI: window.location.origin + window.location.pathname,
        });
      } catch (err) {
        if (err instanceof PrldErrors.BadRequest) {
          setError(`${provider} login is not configured.`);
        } else {
          setError("Something went wrong. 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>Social 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>}
              <div style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
                {providers.map((provider) => (
                  <button key={provider} onClick={() => handleLogin(provider)}>
                    Sign in with {provider.charAt(0).toUpperCase() + provider.slice(1)}
                  </button>
                ))}
              </div>
            </>
          )}
        </main>
      </>
    );
  }
  ```
</Accordion>
