Single Sign-On (SSO) with Backend SDKs
Note
This is the preferred way to add SSO when you keep your own sessions: your server starts SSO and exchanges the code, so the frontend mostly redirects. For the full walkthrough (configure a tenant, then wire start + callback), see Getting Started with SSO.
If you're building with Descope Flows instead, start with the Flows quickstart.
Use the backend SDK when you want your server to drive the SSO handshake. Your backend starts the login, exchanges the authorization code (so it never sits in the browser), and validates the session afterward.
SSO is configured per tenant (SAML or OIDC). See tenant management for how to set that up.
Note
Before the code below will work, enable SSO at the project level and configure a SAML or OIDC connection for at least one tenant. For a full walkthrough, see Getting Started with SSO, or test without your own IdP.
How the Flow Works
There are three steps:
- Start SSO. Your backend calls
sso.startand gets back a URL. Redirect the user's browser to it. Descope sends them to the tenant's identity provider, and after they authenticate, the browser returns to yourredirect_urlwith a one-timecode. - Exchange the code. Your backend calls
sso.exchangewith thatcodeand receives the session and refresh tokens. - Validate the session. Set the session as a cookie (or return it to your client), then validate it on later requests.
Descope acts as the SAML/OIDC service provider toward the identity provider, so you don't build the federation yourself.
Your backend owns the handshake: it starts SSO, exchanges the code server-side, and validates the session. The browser only talks to the IdP and your callback.
Install and Initialize
Install SDK
npm i --save @descope/nextjs-sdkImport and initialize SDK
import { createSdk } from '@descope/nextjs-sdk/server';
// createSdk wraps the backend SDK, so sdk.sso and sdk.management are available.
export const sdk = createSdk({
projectId: process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID!,
});Step 1: Start SSO
When the user clicks sign in with SSO, call the start function from your backend. It returns a URL you redirect the browser to, which kicks off login with the tenant's identity provider.
Identifying the Tenant (tenantIdOrEmail)
The first argument is a tenant ID, a tenant name, or the user's email:
- Email: Descope matches the email domain to a tenant SSO Domain and starts that tenant's SSO connection. For example, the user enters
alex@acme.com. You callsso.start('alex@acme.com', …), and Descope returns the Acme IdP URL. - Tenant ID or name: Skip the domain lookup when your app already knows the org, whether from a subdomain, an org picker, or an invite.
Configure SSO Domains before relying on email. See Tenant identification methods. For several IdPs under one tenant, also set domains per connection or pass ssoId (Multiple SSO providers).
Start Options
| Parameter | Applies to | Purpose |
|---|---|---|
tenantIdOrEmail (required) | All | Tenant ID, tenant name, or email for SSO Domain lookup |
redirectUrl / return_url | All | Where Descope sends the browser after IdP auth, with ?code=… |
loginHint / login_hint | OIDC (often); some SAML IdPs | Hint to the IdP about which account to use, usually the same email the user typed |
ssoId / sso_id | Multi-SSO tenants | Select a specific SSO configuration on the tenant |
prompt | OIDC | IdP prompt (login, consent, none, …). See Prompt. |
forceAuthn / force_authn | SAML | Require fresh authentication at the IdP even if a session exists |
enforceInitiatedEmail | SAML & OIDC | Require the IdP's response to match the email you started with; blocks sign-in otherwise. See below |
loginOptions | All | stepup, mfa, customClaims, templateOptions |
| refresh / MFA token | When step-up or MFA | Current refresh JWT so Descope can elevate the existing session |
Node / Web JS positional order for sso.start:
tenantIdOrEmail, redirectUrl, loginOptions, token, ssoId, forceAuthn, loginHint, enforceInitiatedEmail.
Python uses named args (tenant, return_url, login_options, prompt, sso_id, login_hint, force_authn, …). Check your SDK version for exact names.
Note
enforceInitiatedEmail can be used in the Node.js/TypeScript backend SDK and the client-side JS SDKs (React, Web Component, Web JS). If you're using a different SDK that doesn't support it, call the REST API's initiatedEmail query parameter directly instead (see below).
Verify the IdP Email Matches the Initiated Email
If a user starts SSO with their own email (SP-initiated), you can require Descope to confirm the identity provider authenticated that same person before it completes sign-in. The IdP session in the user's browser might belong to a different account than the one they typed into your app.
Pass enforceInitiatedEmail: true (or initiatedEmail=<email> on the REST API) alongside the email you're starting with:
const email = 'alex@acme.com';
const resp = await descopeClient.sso.start(
email, // SSO Domain → tenant + IdP URL, and the value enforceInitiatedEmail checks against
redirectUrl,
undefined, // loginOptions
undefined, // refresh token
undefined, // ssoId
false, // forceAuthn
email, // loginHint
true, // enforceInitiatedEmail
);REST equivalent: add initiatedEmail=alex@acme.com to the Start SSO query parameters.
This check:
- Compares the email you passed in against the SAML email attribute or NameID (or, for OIDC, the email claim), case-insensitively.
- Fails
sso.exchangewithE062020and blocks sign-in if they don't match. See SSO troubleshooting. - This only applies when SSO is started with an email address. It doesn't apply to SSO started by tenant ID or tenant name, or to IdP-initiated logins, as in both cases there's no email to compare against.
The same option is available from a Descope Flow via the SSO action's Verify initiated email matches IdP response toggle. See SSO with Flows.
Example: Email and Login Hint (OIDC-Friendly)
const email = 'alex@acme.com';
const redirectUrl = 'https://app.example.com/auth/sso/callback';
const resp = await descopeClient.sso.start(
email, // SSO Domain → tenant + IdP URL
redirectUrl,
undefined, // loginOptions
undefined, // refresh token
undefined, // ssoId
false, // forceAuthn
email, // loginHint
);url = descope_client.sso.start(
tenant="alex@acme.com",
return_url="https://app.example.com/auth/sso/callback",
login_hint="alex@acme.com",
)Example: Known Tenant with a Multi-SSO Profile
url = descope_client.sso.start(
tenant="acme-tenant-id",
return_url="https://app.example.com/auth/sso/callback",
sso_id="contractors", # which IdP on that tenant
)REST equivalent: Start SSO (tenant, redirectUrl, loginHint, forceAuthn, prompt, …).
Start Code Samples
import { NextRequest, NextResponse } from 'next/server';
import { sdk } from '@/lib/descope';
// POST /api/auth/sso body: { "email": "alex@acme.com" }
export async function POST(req: NextRequest) {
const { email } = await req.json();
const redirectUrl = 'https://app.example.com/api/auth/sso/callback';
// Descope matches the email domain to the tenant's SSO Domain and returns
// that tenant's IdP authorization URL. loginHint pre-fills the user at OIDC IdPs.
const resp = await sdk.sso.start(email, redirectUrl, undefined, undefined, undefined, false, email);
if (!resp.ok) {
return NextResponse.json(resp.error, { status: 400 });
}
// Redirect the user's browser to the returned URL.
return NextResponse.redirect(resp.data.url);
}Step 2: Exchange the Code
After the user authenticates with the IdP, they're sent back to the redirect_url you passed to sso.start. Pull the code query parameter from that URL and exchange it for tokens:
import { NextRequest, NextResponse } from 'next/server';
import { sdk } from '@/lib/descope';
// GET /api/auth/sso/callback?code=...
export async function GET(req: NextRequest) {
const code = req.nextUrl.searchParams.get('code');
if (!code) {
return NextResponse.json({ error: 'missing code' }, { status: 400 });
}
const resp = await sdk.sso.exchange(code);
if (!resp.ok) {
return NextResponse.json(resp.error, { status: 401 });
}
// resp.data has sessionJwt, refreshJwt, and user. Validate it, then start your session.
return NextResponse.redirect('https://app.example.com/');
}Step 3: Validate the Session
Once you have the tokens, validate the user session on later requests. Descope covers session timeouts, logout, and related options. See backend session validation for details and sample code.