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-configurationScopes and Claims
openid: базовые OIDC claims (iss,sub,aud,exp,iat,noncewhen provided).profile:preferred_username,picture,profile.steam_profile: custom claimsteam_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 использует 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/verifyBilling-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-paidRedirect / 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 недействителен/просрочен/отозван.