Client Cookbook
Copy-and-adapt client code for signing users in with the auth platform (HAP). The examples implement one thing — a standards-compliant OpenID Connect Authorization-Code + PKCE client — because that single client works for all redirect-based modes:
- Hosted Login — point the authorization endpoint at the platform's hosted endpoint (Hosted Login Guidelines).
- SSO broker — point it at a broker's handoff endpoint (SSO Broker Guidelines).
You switch modes by changing one config value (the authorization endpoint) — not code. Best practice is to read it from per-tenant discovery and override only when using a broker.
Examples are illustrative (trimmed error handling) and favour a maintained OIDC/OAuth library over hand-rolled crypto. Always: PKCE,
state, exactredirect_uri, and verify the ID token (iss/aud/exp/nonce). Storeclient_secretin your secret manager, never in code.
Configuration (all languages)
HAP_ISSUER=https://auth.wbsp-demo.com/t/<tenant> # per-tenant issuer
HAP_DISCOVERY=$HAP_ISSUER/.well-known/openid-configuration
HAP_CLIENT_ID=<your client_id>
HAP_CLIENT_SECRET=<your client_secret> # confidential web app; from secret store
HAP_REDIRECT_URI=https://yourapp.example/api/auth/callback # exact match, registered
# Mode switch — leave unset to use discovery's authorization_endpoint (Hosted Login);
# set it to a broker handoff URL to use the SSO broker. Everything else is identical.
HAP_AUTHORIZATION_ENDPOINT= # e.g. https://broker.example/sso/handoffIn-cluster (same K8s cluster as HAP): use the in-cluster Service for back-channel calls (discovery, token, JWKS) but keep the public issuer for
iss/audand the browser redirect (split-horizon — see API Reference §1).
TypeScript / Node (openid-client)
import * as client from "openid-client";
const issuer = new URL(process.env.HAP_ISSUER!);
const config = await client.discovery(issuer, process.env.HAP_CLIENT_ID!, process.env.HAP_CLIENT_SECRET!);
// Mode switch: override the authorization endpoint for broker mode; otherwise use discovery's.
const authorizationEndpoint =
process.env.HAP_AUTHORIZATION_ENDPOINT || config.serverMetadata().authorization_endpoint!;
// --- Start login: redirect the browser ---
export async function startLogin(req, res) {
const verifier = client.randomPKCECodeVerifier();
const challenge = await client.calculatePKCECodeChallenge(verifier);
const state = client.randomState();
req.session.pkce = { verifier, state }; // server-side only
const url = new URL(authorizationEndpoint);
url.search = new URLSearchParams({
response_type: "code",
client_id: process.env.HAP_CLIENT_ID!,
redirect_uri: process.env.HAP_REDIRECT_URI!,
scope: "openid profile email",
code_challenge: challenge, code_challenge_method: "S256",
state,
}).toString();
res.redirect(url.toString());
}
// --- Callback: verify + exchange + mint your session ---
export async function callback(req, res) {
const { verifier, state } = req.session.pkce ?? {};
const tokens = await client.authorizationCodeGrant(config, new URL(req.url, "https://x"), {
pkceCodeVerifier: verifier,
expectedState: state,
});
const claims = tokens.claims()!; // id_token already verified vs JWKS/iss/aud
const user = await upsertUserBySub(claims.sub, claims.email as string, claims.name as string);
req.session.userId = user.id; // your own app session
res.redirect("/");
}Python (authlib, e.g. Flask)
import os
from authlib.integrations.flask_client import OAuth
oauth = OAuth(app)
oauth.register(
name="hap",
client_id=os.environ["HAP_CLIENT_ID"],
client_secret=os.environ["HAP_CLIENT_SECRET"],
server_metadata_url=os.environ["HAP_DISCOVERY"], # discovery → endpoints + JWKS
client_kwargs={"scope": "openid profile email", "code_challenge_method": "S256"},
)
@app.route("/login")
def login():
redirect_uri = os.environ["HAP_REDIRECT_URI"]
# Mode switch: pass authorization_endpoint=<broker handoff> to use the broker; omit for Hosted Login.
override = os.environ.get("HAP_AUTHORIZATION_ENDPOINT")
kwargs = {"authorization_endpoint": override} if override else {}
return oauth.hap.authorize_redirect(redirect_uri, **kwargs) # Authlib adds PKCE + state
@app.route("/api/auth/callback")
def callback():
token = oauth.hap.authorize_access_token() # verifies state, exchanges code, validates id_token
claims = token["userinfo"] # sub / email / name
user = upsert_user_by_sub(claims["sub"], claims.get("email"), claims.get("name"))
session["user_id"] = user.id
return redirect("/")Go (golang.org/x/oauth2 + github.com/coreos/go-oidc/v3)
provider, _ := oidc.NewProvider(ctx, os.Getenv("HAP_ISSUER")) // discovery
verifier := provider.Verifier(&oidc.Config{ClientID: os.Getenv("HAP_CLIENT_ID")})
endpoint := provider.Endpoint() // Hosted Login
if a := os.Getenv("HAP_AUTHORIZATION_ENDPOINT"); a != "" { // mode switch: broker
endpoint.AuthURL = a
}
conf := &oauth2.Config{
ClientID: os.Getenv("HAP_CLIENT_ID"),
ClientSecret: os.Getenv("HAP_CLIENT_SECRET"),
RedirectURL: os.Getenv("HAP_REDIRECT_URI"),
Endpoint: endpoint,
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
// Start login
func startLogin(w http.ResponseWriter, r *http.Request) {
verifierStr := oauth2.GenerateVerifier()
state := randomState()
saveSession(w, r, state, verifierStr) // server-side
http.Redirect(w, r, conf.AuthCodeURL(state,
oauth2.S256ChallengeOption(verifierStr)), http.StatusFound)
}
// Callback
func callback(w http.ResponseWriter, r *http.Request) {
st, verifierStr := loadSession(r)
if r.URL.Query().Get("state") != st { http.Error(w, "bad state", 400); return }
tok, _ := conf.Exchange(r.Context(), r.URL.Query().Get("code"),
oauth2.VerifierOption(verifierStr))
idTok, _ := verifier.Verify(r.Context(), tok.Extra("id_token").(string))
var claims struct{ Sub, Email, Name string }
_ = idTok.Claims(&claims)
upsertUserBySub(claims.Sub, claims.Email, claims.Name) // mint your session
}Raw HTTP (any language) — the contract underneath
# 1. Browser → authorization endpoint (Hosted Login endpoint, or broker handoff URL)
GET {AUTHORIZATION_ENDPOINT}?response_type=code&client_id=…&redirect_uri=…
&scope=openid%20profile%20email&code_challenge=…&code_challenge_method=S256&state=…
# 2. Browser returns to your callback:
GET {redirect_uri}?code=…&state=… # (or ?error=login_required&state=… for prompt=none w/o session)
# 3. Server exchanges the code (token endpoint from discovery):
POST {token_endpoint}
grant_type=authorization_code&code=…&redirect_uri=…&code_verifier=…
&client_id=…&client_secret=…
# → { access_token, id_token, refresh_token }
# 4. Verify id_token signature against {jwks_uri}; check iss, aud, exp, nonce; key user on `sub`.Embedded mode (no redirect) — for completeness
If instead of redirecting you want to render your own login UI (embedded/headless mode), call the
auth API directly (register/login or passwordless OTP) to obtain a login_token, then drive
/authorize + /token server-side. See the Integration Guide. The
token-exchange and verification halves are the same as above; only the login step differs.
Checklist for any client
- PKCE (S256) — verifier kept server-side, never sent to the authorization endpoint.
-
stategenerated and verified on callback (CSRF). -
redirect_uriidentical at authorize, token, and registration (exact match). - ID token verified: signature (JWKS),
iss= public issuer,aud= yourclient_id,exp,nonce. - User keyed on stable
sub; JIT-provision on first arrival; treatemailas changeable. -
client_secretfrom a secret store; never committed. - Authorization endpoint is configuration (discovery for Hosted Login; override for broker).
- Refresh-failure ⇒ sign out; optionally subscribe to identity-lifecycle webhooks.