Use this file to discover all available pages before exploring further.
This guide covers how to implement social login (OAuth) in your web application. Make sure you have configured a social login provider on your backend before proceeding.
Redirect — Your app redirects the user to the provider’s login page
Callback — The provider redirects back to your app with a challenge_token
Finalize — Your app sends the challenge_token to complete authentication
When the OAuth provider has verify_email 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 below.
How is the OAuth flow secured?
The entire flow is protected by PKCE (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
When the provider redirects back to your app, extract the challenge_token from the URL and finalize the login:
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 } }}
When the OAuth provider config has verify_email 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:
The OTP has already been sent to email. Your app shows an OTP screen, the user enters the code, and you call checkOTP:
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.