Technical API Contract: Auth, RBAC, and Billing
Contract version 1.0. This is the implementation source of truth for third-party teams integrating with Doorman SDK and APIs.
Scope and version
This contract defines exactly how a third-party app must integrate: authentication, callback processing, tenant bootstrap, subscription checkout, entitlement gating, usage metering, and RBAC enforcement.
- OIDC issuer baseline:
https://doorman.f1cs-dev.it/oauth2. - Control API baseline:
https://doorman.f1cs-dev.it. - Checked OpenAPI contract:
services/control-api/contracts/control-api.openapi.json. - SDK package baseline:
@doorman/react-sdkplusoidc-client-ts.
Non-negotiable guardrails
- MUST treat Doorman as the identity and entitlement authority.
- MUST unlock paid features only when
getSubscriptionStatus(...).is_active === true. - MUST call
handleDoormanOidcCallbackon callback route. - MUST prefer existing-tenant path:
resolveTenantForIdentity -> ensureBillingCustomerbefore bootstrap. - MUST keep
x-doorman-api-keyoff browser bundles in production (server/agent only). - MUST enforce RBAC from the access token claim for every protected API action in your resource server.
- MUST send usage with stable idempotency keys to prevent overcounting.
- MUST fail closed on auth/callback/token errors (no partial login state).
Required configuration
Minimum runtime env for SPA/server integration:
OIDC_ISSUER=https://doorman.f1cs-dev.it/oauth2 OIDC_CLIENT_ID=<oauth_client_id> OIDC_REDIRECT_URI=<app_origin>/callback OIDC_POST_LOGOUT_REDIRECT_URI=<app_origin>/ OIDC_SCOPE=openid profile email offline_access KRATOS_PUBLIC_URL=http://localhost:4433 CONTROL_API_URL=https://doorman.f1cs-dev.it DOORMAN_APP_ID=<app_id> # Server integrations only; never NEXT_PUBLIC_/VITE_: DOORMAN_API_KEY=<scoped_dmn_...>
SSO provider callback registered in Google/Microsoft/GitHub: https://doorman.f1cs-dev.it/api/oauth/sso/callback .
SDK surface to use
- UI-first auth path:
DoormanAuthPanelfor register/login/direct SSO/domain SSO. - Low-level auth path:
createDoormanCredentialAuthClient,startDoormanLogin,handleDoormanOidcCallback. - SSO routing/admin:
createDoormanSsoClient. - Billing/runtime:
createDoormanBillingClient. - Authorization:
getDoormanClaimsFromUser,createDoormanPolicyContext,can,requirePolicy,DoormanProvider,RequirePolicy,createDoormanServerContext, andrequireServerPolicy.
import {
DoormanAuthPanel,
buildDoormanOidcClientSettings,
createDoormanBillingClient,
createDoormanServerContext,
requireServerPolicy,
RequirePolicy,
handleDoormanOidcCallback
} from "@doorman/react-sdk";Auth sequence contract
- User starts login/register/SSO from app UI.
- App uses SDK auth flow and redirects to Doorman/Hydra authorize. App MUST request the app audience:
audience=urn:doorman:app:${APP_ID}. - Provider or credential auth completes, then user returns to app callback URL with code/state.
- App callback route calls
handleDoormanOidcCallback(...). - App fetches authenticated user and reads
subclaim as identity_id for downstream billing APIs.
const result = await handleDoormanOidcCallback(userManager, {
pathname: window.location.pathname,
search: window.location.search,
callbackPath: "/callback",
clearUrlPath: "/"
});
if (result.handled && result.errorMessage) throw new Error(result.errorMessage);Billing sequence contract
- Resolve tenant by identity:
resolveTenantForIdentity(identity_id=sub). - If no tenant: bootstrap once with
bootstrapTenant(identity_id, tenant_name, email). - Ensure customer mapping for effective tenant:
ensureBillingCustomer(tenant_id). - Start checkout:
createCheckoutSession(tenant_id, plan_id, success_url, cancel_url). - Redirect user to returned checkout_url.
- On success callback, poll
getSubscriptionStatus(subscription_id)until active or timeout. - Send usage with meter id via
ingestUsageEventafter activation.
SDK to API endpoint matrix
| SDK method | HTTP | Endpoint | Auth mode | Notes |
|---|---|---|---|---|
| createDoormanSsoClient.resolveSsoProviderByEmail | GET | /api/sso/resolve-provider?email=... | Browser session | Enterprise domain-discovery SSO only. |
| createDoormanCredentialAuthClient.registerWithPassword | GET+POST | /self-service/registration/browser and /self-service/registration | Kratos browser session | In-app credential registration path. |
| createDoormanCredentialAuthClient.loginWithPassword | GET+POST | /self-service/login/browser?refresh=true and /self-service/login | Kratos browser session | Refresh=true allows already-authenticated browser sessions. |
| startDoormanLogin | GET | Hydra /oauth2/auth via oidc-client-ts | Browser redirect | Supports provider-first hints via extraQueryParams. MUST request audience=urn:doorman:app:${APP_ID} for stable aud enforcement. |
| handleDoormanOidcCallback | GET callback | App callback route (for example /callback) | Browser redirect callback | Must be called exactly once per callback URL. |
| getDoormanClaimsFromUser | local | OIDC user access_token/id_token/profile | Validated JWT claims | Deny-safe claim parser for app id, tenants, roles, permissions, entitlements. |
| can / requirePolicy | local | SDK policy evaluator | Parsed Doorman claims | Evaluates generic rules only; app owns product-specific permission meaning. |
| createDoormanServerContext / requireServerPolicy | local | Resource server action/API handler | Bearer JWT or explicit claims | Use for protected mutations. React gates are not security boundaries. |
| direct HTTP (app admin) | GET+POST | /api/apps/:appId/oauth/clients | x-doorman-api-key (server only) | Optional: manage OIDC clients programmatically (non-interactive rollout, service agents). |
| direct HTTP (app admin) | GET+POST | /api/apps/:appId/service-accounts | x-doorman-api-key (server only) | Optional: manage service accounts via API key instead of dashboard session cookie. |
| billing.resolveTenantForIdentity | POST | /api/apps/:appId/billing/resolve-tenant | x-doorman-api-key | No Stripe side effects. |
| billing.bootstrapTenant | POST | /api/apps/:appId/billing/bootstrap-tenant | x-doorman-api-key | First-time identity bootstrap only. |
| billing.ensureBillingCustomer | POST | /api/apps/:appId/billing/ensure-customer | x-doorman-api-key | Existing tenant path before checkout. |
| billing.createCheckoutSession | POST | /api/apps/:appId/billing/checkout-sessions | x-doorman-api-key | Creates/updates subscription and returns checkout_url. |
| billing.getSubscriptionStatus | GET | /api/apps/:appId/billing/subscriptions/:subscriptionId/status | x-doorman-api-key | Gate entitlements on is_active=true only. |
| billing.ingestUsageEvent | POST | /api/apps/:appId/events/ingest | x-doorman-api-key | Idempotency key strongly recommended. |
| billing.createAgent | POST | /api/apps/:appId/service-accounts | x-doorman-api-key | App-managed service agent lifecycle. |
| billing.listAgents | GET | /api/apps/:appId/service-accounts | x-doorman-api-key | Use for agent inventory and limits. |
Full OpenAPI reference:https://doorman.f1cs-dev.it/docs/api/
RBAC token contract
Resource servers must enforce tenant-scoped permissions from the Doorman access-token claim. Consumer apps define permission names and resource rules; Doorman only supplies generic claims and evaluator mechanics.
Audience contract: access tokens MUST include your app audience in the standard aud claim. Required value:urn:doorman:app:${APP_ID}If aud is empty, ensure your authorize request includesaudience=urn:doorman:app:${APP_ID}
{
"doorman": {
"v": 1,
"app_id": "APP_ID",
"tenants": [
{
"tenant_id": "TENANT_ID",
"roles": ["member", "admin"],
"permissions": ["resources:read", "resources:write"]
}
]
}
}- Verify JWT signature and standard claims (
iss,aud,exp). - Verify
audcontainsurn:doorman:app:${APP_ID}. - Verify
doorman.app_idmatches your app id. - Find tenant block for the requested tenant id.
- Require permission presence before executing action.
- Deny by default if claim missing or malformed.
const context = createDoormanServerContext({
authorizationHeader: request.headers.get("authorization"),
appId: "APP_ID",
activeTenantId: tenantId
});
requireServerPolicy(context, {
type: "permission",
permission: "resources:write"
});Client helpers such as DoormanProvider and RequirePolicy only hide or show UI. Server-side enforcement is mandatory for protected actions.
Service agent contract
Agent-like entities must be created as Doorman billing agents/service accounts and tied to tenant + role_ids.
- Create:
createAgent(name, tenant_id, role_ids?). - Rotate credentials:
rotateAgentSecret(agent_id). - Disable or revoke:
deactivateAgentandrevokeAgent. - Enforce caps from plan capacity metrics:
service_accountsandmember_seats.
Error handling contract
All billing API failures surface as DoormanApiError with status , code , and raw details .
try {
await billing.createCheckoutSession({ tenant_id, plan_id, success_url, cancel_url });
} catch (error) {
if (error instanceof DoormanApiError) {
// Handle by error.code, not message text.
console.error(error.status, error.code, error.details);
}
}- Treat these as operator-mapping errors:
missing_provider_plan_base_component_mapping,missing_provider_customer_mapping. - Treat these as limits/business errors:
tenant_app_member_cap_exceeded,service_account_cap_exceeded. - Treat auth callback stale PKCE as retryable and restart login from app root.
Conformance tests
A third-party implementation is compliant only if all tests pass:
- Password register/login and Google SSO both end with authenticated app session and no callback loop.
- Callback route is idempotent (reload callback URL does not break auth).
- Checkout creates pending subscription, then flips to active, and app unlocks only after active.
- Usage ingestion produces Doorman usage/event records and Stripe export after worker sync.
- Protected API endpoints reject missing/invalid permission claims.
- Agent creation obeys plan cap and returns deterministic cap-exceeded errors when limit reached.