SteamAuth HubSteamAuth Hub

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-configuration

Scopes 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_token

Confidential 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/verify

Billing-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-paid

Redirect / 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.
Docs | SteamAuth Hub