Skip to content

Tutorial

Build your first confidential Web integration

This walkthrough uses a server-side application. Your client secret and refresh tokens must never reach the browser.

1. Register the application

  1. Create an application in the developer portal.
  2. Add a Confidential Web backend client.
  3. Register the exact callback URI. Start with http://localhost:3000/auth/stoatboard/callback, then add the HTTPS production URI separately.
  4. Copy the client secret once and store it in your server secret manager.

2. Discover the provider and start authorization

Install a maintained OIDC client such as openid-client. Generate a new PKCE verifier, state and nonce for every attempt; keep them in the user's server-side session.

import * as oidc from "openid-client";

const issuer = new URL("https://stoatboard.com");
const config = await oidc.discovery(issuer, CLIENT_ID, CLIENT_SECRET);

const codeVerifier = oidc.randomPKCECodeVerifier();
const codeChallenge = await oidc.calculatePKCECodeChallenge(codeVerifier);
const state = oidc.randomState();
const nonce = oidc.randomNonce();

// Save these values in the user's encrypted server-side session.
session.oidc = { codeVerifier, state, nonce };

const authorizationUrl = oidc.buildAuthorizationUrl(config, {
  redirect_uri: "https://example.com/auth/stoatboard/callback",
  scope: "openid profile stoat",
  code_challenge: codeChallenge,
  code_challenge_method: "S256",
  state,
  nonce,
});

return redirect(authorizationUrl.href);
The browser may carry the opaque OIDC interaction cookie and the authorization response. It must not carry a client secret, a Stoat session token, or an arbitrary return URL invented by your application.

3. Validate the callback

Your library performs discovery, exchanges the one-time code using client_secret_basic and the PKCE verifier, validates the issuer, audience, signature, state and nonce, then exposes the claims.

const currentUrl = new URL(request.url);
const saved = session.oidc;
if (!saved) throw new Error("Missing OIDC transaction");

const tokens = await oidc.authorizationCodeGrant(config, currentUrl, {
  pkceCodeVerifier: saved.codeVerifier,
  expectedState: saved.state,
  expectedNonce: saved.nonce,
  idTokenExpected: true,
});

delete session.oidc; // one-time transaction state
const claims = tokens.claims();
if (!claims?.sub) throw new Error("Missing subject");

// Use (issuer, sub) as the external identity key.
await signInOrCreateUser({ issuer: issuer.href, subject: claims.sub });

4. Request only what you use

  • openid is required and gives you a stable opaque StoatBoard subject.
  • profile adds username and picture.
  • stoat adds the linked Stoat ID and verification flag.
  • email is optional, can be refused, and is returned only when verified.
  • offline_access enables rotating refresh tokens for confidential Web clients only.

Completion check

Test consent approval and refusal, an expired or replayed callback, an email refusal, and revocation from Authorized Apps. Then continue with the production guides.