SteamAuth Hub

OpenID Connect Integration

Провайдер поддерживает OIDC Discovery + Authorization Code Flow, PKCE для public clients, JWT `id_token` через JWKS и `userinfo`. Steam остаётся внутренним upstream-провайдером.

Discovery

Подключайте любой стандартный OIDC client через discovery URL:

GET https://auth.yourdomain.com/broker/.well-known/openid-configuration

Scopes and Claims

  • openid: базовые 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 использует public API. Ниже минимальная цепочка, чтобы внешний tenant самостоятельно дошёл до рабочего 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)

Платёжный gateway пока не подключён. Free доступ моментальный, апгрейд выполняется через request flow и manual invoice lifecycle в 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 (scheme+host+path), без wildcard.
  • Query/fragment у redirect URI запрещены.
  • Production redirect URI должен указывать на verified domain этого же tenant.
  • Localhost/loopback разрешается только для test/dev clients (`isTestClient=true`) или в non-production env.
  • Apex/subdomain conflict policy: если org A владеет `example.com`, org B не может заявить ни `example.com`, ни `app.example.com`.

Troubleshooting

  • invalid_redirect_uri: `redirect_uri` должен совпадать 1:1 с URI, добавленным в проект.
  • invalid_grant: просроченный/использованный code или неверный PKCE verifier.
  • invalid_grant_reuse_detected: reuse refresh token, текущая токен-сессия отозвана.
  • invalid_scope: client запросил scope вне allowed scopes.
  • invalid_token: access token недействителен/просрочен/отозван.