OpenID Connect Integration
The provider supports OIDC Discovery + Authorization Code Flow, PKCE for public clients, JWT `id_token` via JWKS and `userinfo`. Steam stays an internal upstream provider.
Discovery
Connect any standard OIDC client via discovery URL:
GET https://auth.yourdomain.com/broker/.well-known/openid-configurationScopes and Claims
- `openid`: base OIDC claims (`iss`, `sub`, `aud`, `exp`, `iat`, `nonce` when provided).
- `profile`: `preferred_username`, `picture`, `profile`.
- `steam_profile`: custom claim `steam_id`.
Public Client (PKCE S256)
import crypto from "node:crypto";
const verifier = crypto.randomBytes(32).toString("base64url");
const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
const authUrl = new URL("https://auth.yourdomain.com/broker/v1/oauth/authorize");
authUrl.searchParams.set("response_type", "code");
authUrl.searchParams.set("client_id", process.env.OIDC_CLIENT_ID!);
authUrl.searchParams.set("redirect_uri", process.env.OIDC_REDIRECT_URI!);
authUrl.searchParams.set("scope", "openid profile steam_profile");
authUrl.searchParams.set("state", crypto.randomUUID());
authUrl.searchParams.set("nonce", crypto.randomUUID());
authUrl.searchParams.set("code_challenge", challenge);
authUrl.searchParams.set("code_challenge_method", "S256");
res.redirect(authUrl.toString());const tokenRes = await fetch("https://auth.yourdomain.com/broker/v1/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "authorization_code",
client_id: process.env.OIDC_CLIENT_ID,
code: req.query.code,
redirect_uri: process.env.OIDC_REDIRECT_URI,
code_verifier: verifier
})
});
const tokens = await tokenRes.json();
// tokens: access_token, refresh_token, id_tokenConfidential Client
const tokenRes = await fetch("https://auth.yourdomain.com/broker/v1/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "authorization_code",
client_id: process.env.OIDC_CLIENT_ID,
client_secret: process.env.OIDC_CLIENT_SECRET,
code: req.query.code,
redirect_uri: process.env.OIDC_REDIRECT_URI
})
});const basic = Buffer.from(
process.env.OIDC_CLIENT_ID + ":" + process.env.OIDC_CLIENT_SECRET
).toString("base64");
const tokenRes = await fetch("https://auth.yourdomain.com/broker/v1/oauth/token", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Basic " + basic
},
body: JSON.stringify({
grant_type: "authorization_code",
code: req.query.code,
redirect_uri: process.env.OIDC_REDIRECT_URI
})
});UserInfo
const userinfoRes = await fetch("https://auth.yourdomain.com/broker/v1/oauth/userinfo", {
headers: {
Authorization: "Bearer " + tokens.access_token
}
});
const userinfo = await userinfoRes.json();
// sub, preferred_username, picture, profile, steam_id (if scope steam_profile)Refresh, Revoke, Logout
const refreshRes = await fetch("https://auth.yourdomain.com/broker/v1/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "refresh_token",
client_id: process.env.OIDC_CLIENT_ID,
client_secret: process.env.OIDC_CLIENT_SECRET, // optional for public
refresh_token: tokens.refresh_token
})
});await fetch("https://auth.yourdomain.com/broker/v1/oauth/revoke", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
client_id: process.env.OIDC_CLIENT_ID,
token: tokens.access_token,
token_type_hint: "access_token"
})
});const endSession = new URL("https://auth.yourdomain.com/broker/v1/oauth/logout");
endSession.searchParams.set("client_id", process.env.OIDC_CLIENT_ID!);
endSession.searchParams.set("post_logout_redirect_uri", "https://client.example.com/");
res.redirect(endSession.toString());Self-service Onboarding API
Dashboard uses the public API. Below is a minimal chain to let an external tenant self-onboard to a working OIDC client.
POST /v1/orgs
POST /v1/orgs/:id/projects
POST /v1/projects/:id/clients
POST /v1/clients/:id/redirect-uris
POST /v1/projects/:id/domains
POST /v1/domains/:id/verification-challenge
POST /v1/domains/:id/verifyBilling-ready API (No Checkout)
Payment gateway is not connected yet. Free access is instant, upgrade goes via request flow and manual invoice lifecycle in backoffice.
GET /v1/plans
GET /v1/orgs/:id/billing
GET /v1/orgs/:id/entitlements
POST /v1/orgs/:id/upgrade-requests
GET /v1/orgs/:id/invoices
GET /v1/orgs/:id/billing/history
# Operator backoffice:
GET /v1/admin/upgrade-requests
POST /v1/admin/upgrade-requests/:id/approve
POST /v1/admin/contracts/:id/activate
POST /v1/admin/invoices/:id/issue
POST /v1/admin/invoices/:id/mark-paidRedirect / Domain Policy
- Exact redirect match only (`scheme+host+path`), no wildcard.
- Query/fragment are forbidden in redirect URI.
- Production redirect URI must point to a verified domain of the same tenant.
- Localhost/loopback is allowed only for test/dev clients (`isTestClient=true`) or in non-production env.
- Apex/subdomain conflict policy: if org A owns `example.com`, org B cannot claim `example.com` or `app.example.com`.
Troubleshooting
- `invalid_redirect_uri`: `redirect_uri` must match 1:1 with URI registered in project.
- `invalid_grant`: expired/used code or wrong PKCE verifier.
- `invalid_grant_reuse_detected`: refresh token reuse detected, current token session revoked.
- `invalid_scope`: client requested scope outside allowed scopes.
- `invalid_token`: access token is invalid/expired/revoked.