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
- Create an application in the developer portal.
- Add a Confidential Web backend client.
- Register the exact callback URI. Start with
http://localhost:3000/auth/stoatboard/callback, then add the HTTPS production URI separately. - 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
openidis required and gives you a stable opaque StoatBoard subject.profileadds username and picture.stoatadds the linked Stoat ID and verification flag.emailis optional, can be refused, and is returned only when verified.offline_accessenables 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.