JavaScript and backend integration
Use a backend for the hosted Sigma authorization-code flow. The registered application's member WIF is a server credential; do not put it in a JavaScript bundle, native executable, extension, or mobile app.
Create a transaction
The following server-side functions use Node's cryptographic random generator. persistTransaction and consumeTransaction below describe application storage requirements, not exports from the Sigma package.
import { createHash, randomBytes } from "node:crypto";
export function createOAuthTransaction() {
const state = randomBytes(32).toString("base64url");
const verifier = randomBytes(32).toString("base64url");
const challenge = createHash("sha256")
.update(verifier).digest("base64url");
return { state, verifier, challenge, expiresAt: Date.now() + 600_000 };
}Persist { state, verifier, expiresAt, redirectUri, issuer } under an unpredictable browser transaction identifier. Set that identifier in an HttpOnly, Secure, SameSite=Lax cookie on your application's HTTPS origin. Store only a hash of the identifier if your session store supports that pattern. Use a shared server store with expiry and atomic consumption; do not expose the verifier in the redirect URL.
Build the authorization URL
export function authorizationURL(input: {
clientId: string; redirectUri: string; state: string; challenge: string;
}) {
const url = new URL("https://auth.sigmaidentity.com/api/auth/oauth2/authorize");
url.search = new URLSearchParams({
client_id: input.clientId,
redirect_uri: input.redirectUri,
response_type: "code",
scope: "openid profile email",
state: input.state,
code_challenge: input.challenge,
code_challenge_method: "S256",
}).toString();
return url;
}Use a fixed, registered redirectUri. Add offline_access only if the application needs refresh tokens. If you use an ID token as the authentication evidence, generate an OIDC nonce, include it in this request and transaction, and validate it with the token's issuer, audience, signature, and expiry.
Validate and consume the callback
Read the transaction through the browser cookie. Reject missing, duplicated, expired, mismatched, or already-used state, and an iss that differs from the configured issuer. Validate the error callback through the same state check before displaying it. Atomically consume the transaction before exchanging the code. Never log the callback query, verifier, tokens, or WIF.
Exchange on the server
import { exchangeCodeForTokens } from "@sigma-auth/better-auth-plugin/server";
export async function exchangeValidatedCode(input: {
code: string; verifier: string; redirectUri: string;
clientId: string; memberPrivateKey: string;
}) {
return exchangeCodeForTokens({
issuerUrl: "https://auth.sigmaidentity.com",
code: input.code,
codeVerifier: input.verifier,
redirectUri: input.redirectUri,
clientId: input.clientId,
accountPrivateKey: input.memberPrivateKey,
});
}Call this function only after transaction validation. The package signs the form-encoded token request with bitcoin-auth, sends X-Auth-Token, and retrieves userinfo. It returns user, access_token, id_token, expires_in, and an optional refresh_token. The helper requires the openid scope.
Establish your application session
Use the validated issuer and returned user.sub as the external account identity. Keep user.bap_id as the selected profile, not a replacement for the account ID. Apply your account-linking policy, create an opaque local session in your session framework, and return only its HttpOnly cookie to the browser. Keep OAuth tokens server-side when later API access is required.
Use a fixed same-origin destination after login. Set no-store caching and clear the transaction cookie on all final callback paths. On logout, invalidate your local session and decide separately whether to revoke Sigma refresh access or end the Sigma session.
For Better Auth session creation in Next.js, use the callback adapter after these checks.