Next.js integration
This guide assumes an existing, database-backed Better Auth application. Keep its database adapter, BETTER_AUTH_SECRET, trusted origins, and account policy configured. Sigma adds an external sign-in method; it does not replace that setup.
Mount Better Auth
import { toNextJsHandler } from "better-auth/next-js";
import { auth } from "@/lib/auth";
export const { GET, POST } = toNextJsHandler(auth);Configure the four application variables in the quickstart, including an explicit NEXT_PUBLIC_APP_URL. Register ${NEXT_PUBLIC_APP_URL}/auth/sigma/callback with Sigma. Keep the issuer fixed in server configuration; never accept a token endpoint or issuer URL from callback input.
Choose the callback helper
import { createBetterAuthCallbackHandler } from "@sigma-auth/better-auth-plugin/next";
import { auth } from "@/lib/auth";
const exchangeAndCreateSession = createBetterAuthCallbackHandler({
auth,
issuerUrl: "https://auth.sigmaidentity.com",
callbackPath: "/auth/sigma/callback",
});This helper accepts a POST request containing code and code_verifier, exchanges the code with the server member key, upserts the local account, and sets a signed Better Auth session cookie. Its response also contains OAuth tokens. Do not copy them into localStorage simply to render a signed-in UI.
The default helper finds users by email and can create users. Apps that already have accounts should supply findUser, createUser, and updateUser callbacks to enforce their linking policy. Prefer an existing provider-account association keyed by issuer and sub; do not silently merge accounts from an unverified profile email. Use disableImplicitSignUp: true when sign-in must never create a local account. Placeholder emails for users without email are identifiers, not verified deliverable addresses.
Validate the transaction before calling the helper
The helper is a token/session adapter, not a complete OAuth callback security boundary. Do not export it directly on a public route without transaction protection.
Your start route must create random state and S256 PKCE, store the verifier server-side in a short-lived transaction tied to a secure HttpOnly browser cookie, and redirect to Sigma. On callback:
- Require exactly one
stateand either onecodeor an OAuth error. - Match state to the browser's unexpired transaction; consume that transaction atomically so another callback cannot reuse it.
- Validate
issagainst the configured issuer, and use the original registered redirect URI. - Read the verifier from the server transaction, not from arbitrary callback JSON.
- Call the helper with the validated code and verifier. Preserve its
Set-Cookieheader on your final response, return a fixed same-origin redirect, and discard the token JSON if your browser does not need it. - Clear the transaction cookie on success or failure and set
Cache-Control: no-storeon these responses.
The JavaScript guide provides the transaction contract and token-exchange code. Your application must supply its durable transaction/session storage. An in-memory map is unsuitable across serverless instances.
The browser plugin's handleCallback() checks its own sessionStorage state, but that client-side check does not protect a directly callable server route. Do not combine a server-owned flow with handleCallback() and exchange the same code twice.
Protect server resources
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { auth } from "@/lib/auth";
export default async function PrivatePage() {
const session = await auth.api.getSession({ headers: await headers() });
if (!session?.user) redirect("/login");
return <p>Signed in as {session.user.name}</p>;
}Apply the same session and authorization checks inside route handlers and server actions. A React redirect or a hidden component is not access control. Use Better Auth's own sign-out API to end the local session; ending a local session does not automatically revoke Sigma tokens or sign out of Sigma.
Verify the integration
Exercise state mismatch, expired/consumed transactions, wrong issuer, wrong verifier, duplicate React callback execution, rejected consent, and local session creation. Check the cookie is accepted behind your reverse proxy. Test against staging with a registered callback; do not create QA accounts in production.