Application integration guide: hosted OIDC flow (Path A)#
There are two supported integration paths. This guide covers Path A, where an operator registers your application and users sign up and sign in on this service's hosted pages. For the self-service REST flow (developer portal,
clientId+ API key exchange,tenant/user/registerandtenant/user/login), see the REST application flow guide, which also has a side-by-side comparison to help you choose.
user-mgt provides per-application identities and tenant authorization. Applications are
registered at runtime; a tenant is one customer account inside an application. Every
application has its own user directory: the same email address is an unrelated account
in each application (and in the developer portal), with its own password and sessions.
See identity scopes.
Roles, permissions, memberships and invitations belong to that tenant.
The registered client selects the application automatically. Users never choose between applications during login. They choose an account only when they have several active memberships inside the initiating application.
Implementation phases 1–6 cover this flow. Deployment and staging verification are deferred; this guide does not claim a provisioned environment.
1. Register the application once#
An active platform operator uses a fresh passkey-backed platform token to call:
POST /platform/applications
Authorization: Bearer <platform access token>
Content-Type: application/json
Supply these runtime values:
| Field | Value |
|---|---|
slug |
Unique lowercase application slug |
name |
Application display name |
allowedOrigins |
Exact HTTPS browser origins |
redirectUris |
Exact OIDC callback URLs within those origins |
postLogoutRedirectUris |
Exact logout return URLs within those origins; may be empty |
owner.email |
Required. Email for the application's own platform administrator |
owner.roleName |
Optional. Display name for that tenant's protected administrator role (defaults to "Tenant Admin") |
owner.tenantSlug / owner.tenantName |
Optional. Default to the application's own slug / name |
An application cannot be registered without an owner: it atomically creates
the application's own platform-administration tenant alongside the application
itself, with a fresh account for that email as its TENANT_ADMIN. No password
is ever caller-asserted — a one-time, time-limited set-password link is emailed
to the owner instead.
HTTP 201 returns:
application._id Runtime application ID
application.tokenAudience Audience for this application's access tokens
client._id Public OIDC client ID
client.redirectUris Registered callbacks
key.id Public key identifier
key.apiKey Backend integration secret, returned once
key.scopes Allowed integration operations
key.expiresAt Expiry of this key
owner.tenant._id The owner's platform-administration tenant
owner.tenant.applicationHome Always true here; tenant-context tokens for this
tenant carry the same flag so your backend can
route its owner away from customer-tenant screens
owner.ownerUserId The owner's newly created user ID
owner.roles Tenant roles, including the protected TENANT_ADMIN
The initial key lasts 90 days and has application:read, onboarding:create
and tenants:create scopes. Store it in the application backend secret store.
It identifies the application; it cannot impersonate users or administer tenants.
The OIDC public client uses PKCE and has no client secret.
Rotation: issue an overlapping replacement through
POST /platform/applications/:id/api-keys, update the backend, then revoke the
old key through DELETE /platform/applications/:id/api-keys/:keyId. Key listing
returns metadata only. Registration and status changes take effect on running
replicas. See operator registration and bootstrap.
Operator bootstrap remains an explicit operational step, not public signup.
2. Configure the backend helper#
The source helper is UserManagementClient.ts.
It compiles with this repository and can be copied into a Node backend; it is
not published as an npm package. It uses Node crypto and fetch, with no service
configuration imports. Existing JWT validation and database-scoping helpers are
in the same SDK directory.
import {
UserManagementClient, createAuthorizationTransaction, UserManagementError,
} from './src/integrations/sdk/UserManagementClient';
const identity = new UserManagementClient({
baseUrl: process.env.IDENTITY_BASE_URL!,
clientId: process.env.IDENTITY_CLIENT_ID!,
audience: process.env.IDENTITY_AUDIENCE!,
redirectUri: process.env.IDENTITY_CALLBACK_URL!,
apiKey: process.env.IDENTITY_API_KEY!,
});
These are consumer-backend variable names, not additional service settings. Use values returned by registration and the operator-provided service origin. The helper requires HTTPS, except loopback HTTP for local development. Requests have a five-second deadline, reject redirects and never retry mutations. It does not store user tokens. Do not bundle the helper or its API key into browser code.
3. Signup: one account, one owner#
For an explicit Create account action:
const transaction = createAuthorizationTransaction();
// Persist the full transaction in the initiating browser's server-side session.
const onboarding = await identity.startOnboarding(transaction);
// Redirect that browser to onboarding.authorizationUrl.
Persist state, nonce, codeVerifier and expiry in a short-lived, single-use
server session bound to that browser. The helper sends only the challenge,
state and nonce to POST /onboarding; the backend key stays in its request header.
Do not create a new intent on every callback retry.
The hosted flow authenticates an existing identity or registers and verifies a
new identity. The user enters customer account details. Completion atomically
creates the tenant, its protected TENANT_ADMIN role and the owner's membership.
Retries of the same intent cannot create extra accounts. TENANT_ADMIN starts
with no application permissions; no default member role is created.
Email verification can happen in another browser, then the user returns to the
original signup tab. Verification alone does not sign a browser in. See
hosted onboarding for expiry and retry behavior.
4. Normal login and the OIDC callback#
Use discovery at /.well-known/openid-configuration. Validate its issuer against
the configured issuer before trusting the advertised endpoints. For normal
login, use the discovered authorization endpoint with:
const transaction = createAuthorizationTransaction();
// Persist the browser-bound transaction before redirecting.
const parameters = identity.loginParameters(transaction);
// Append parameters to the authorization endpoint from validated discovery.
The parameters include the registered client, callback, application audience as
resource, openid email profile, PKCE S256, state and nonce. Normal login
creates no tenant. For long-lived identity sessions, explicitly request
offline_access and consent using your OIDC client integration.
The helper handles user-mgt REST calls; your OIDC client handles the protocol
callback. On callback, consume the saved browser-bound transaction once,
check its expiry and exact state, then exchange the code at the discovered token
endpoint with grant_type=authorization_code, client_id, the exact registered
redirect_uri, and code_verifier. Handle provider errors without creating a
local authenticated session.
Validate the ID token signature using the issuer's JWKS, permitted algorithm,
issuer, client audience, expiry and saved nonce (including azp when applicable).
Use the OIDC client's response validation; decoding a JWT is not validation.
Reject duplicate/missing callback parameters and never accept callback-supplied
issuer, client or destination overrides. The test suite exercises the real
provider, code exchange and rejection of a wrong verifier and code replay.
Keep the contracts distinct:
| Credential | Purpose |
|---|---|
OIDC id_token |
Validated authentication result for the client |
OIDC access_token |
Application-bound identity proof for user-mgt REST |
OIDC refresh_token |
Identity refresh at the OIDC token endpoint |
Context tokens.accessToken |
Authorization inside one tenant account |
| Context refresh ID + secret | Rotate that tenant session at /auth/context/refresh |
X-API-Key |
Backend integration operations; no user authority |
Keep tokens in the backend session and give the browser a Secure, HttpOnly, SameSite session cookie. Apply CSRF protection to your own browser mutation routes.
5. Automatic account selection#
After the validated callback, use the OIDC access token:
try {
const accountSession = await identity.selectAccount(oidcAccessToken);
// Persist its context and token pair in the authenticated backend session.
} catch (error) {
if (error instanceof UserManagementError && error.code === 'ACCOUNT_SELECTION_REQUIRED') {
// Present error.details.memberships, then call selectAccount again with
// the chosen membershipId. Validate the choice through user-mgt.
} else if (error instanceof UserManagementError && error.code === 'NO_ACCOUNTS') {
// Offer explicit account creation or invitation acceptance.
} else {
throw error;
}
}
One active membership selects automatically. Several return HTTP 409 with choices
inside this application. A saved membership choice is always revalidated.
memberships(accessToken) exposes the same filtered discovery separately.
For an additional account, call
createAccount(oidcAccessToken, { name, slug }, idempotencyKey). Generate the key
once per deliberate creation action; reuse it only for retries with identical
details. The server requires matching identity/application-key scope and returns
HTTP 409 if that idempotency key is reused with different details. Login and
invitation acceptance never implicitly invoke this operation.
6. Tenant administration and invitations#
Use a context token whose membership has the protected TENANT_ADMIN role.
The helper supplies the backend key too, so the service checks that both belong
to the same application.
| Helper | REST operation |
|---|---|
registerPermission, permissions, deprecatePermission |
Register/list/deprecate opaque tenant permission keys |
createRole, roles, updateRole, deleteRole |
Manage custom tenant roles; deletion requires a replacement role |
invite, revokeInvitation |
Invite a recipient with explicit same-tenant role IDs; revoke pending invitation |
updateMember |
Assign roles, suspend, restore or remove a membership |
acceptInvitation |
Accept using verified identity proof and the one-time invitation token |
Applications supply their permission catalogs as runtime values. Keys have no required prefix or semantic meaning to user-mgt. Register each key inside each customer tenant, then create custom roles with the selected keys. Custom roles accept 1–64 active keys; memberships accept 1–10 roles. Do not modify the protected admin role to add business permissions. Assign a custom role to an administrator if that user also needs business access.
Permission/role/membership changes invalidate affected contexts. Obtain a fresh context with the identity token before continuing after an authorization change; refresh cannot restore revoked membership sessions. Never blindly retry a failed administrative mutation.
An invitation response contains its ID; the one-time secret is delivered through the email outbox. The hosted email link supports registration, verification and acceptance for the intended recipient. Acceptance adds one membership to the inviting tenant, then returns the user to the registered application's origin to start normal OIDC login. It does not create another tenant or issue an unsolicited OIDC callback. Removing a membership leaves the user's account in your application intact.
7. Refresh, authorization and logout#
refreshContext({ refreshTokenId, refreshToken }) rotates one context session.
Persist the returned pair atomically and serialize concurrent refresh attempts.
A consumed pair cannot be safely retried; replay revokes the family. Refresh
preserves application, tenant, membership and client scope.
Use context(contextAccessToken) for live user-mgt validation. The existing
TenantTokenVerifier verifies signatures and tenant claims locally; it cannot
see logout or server-side revocation until expiry. For live service-to-service
validation, TenantIntrospectionClient calls /internal/tokens/introspect using
an application service token. Service clients use client_credentials with
private_key_jwt; application registration currently creates a public client,
so service-client provisioning remains operator managed. A backend API key is
not a substitute for that service token.
Every consumer business read/write must enforce the validated application's
ID, tenant ID and required opaque permission. TenantBoundRepository supplies
scoped MongoDB query helpers; keep authorization context server-constructed.
sessions(identityAccessToken) lists the user's global sessions.
revokeSession(identityAccessToken, sessionId) revokes an owned session.
Revoking an OIDC grant (oidc_ ID) also invalidates its derived tenant sessions.
logoutAll(identityAccessToken) invalidates this account's identity, hosted browser,
and context sessions within your application. Clear the consumer's own cookie/session
after logout. OIDC end-session/revocation use the discovered protocol endpoints.
See session semantics for independent grants.
8. Account security#
/users/me and /auth/passkeys/* require verified identity access, not tenant or
support tokens. These actions affect the user's account in your application only:
PATCH /users/me: change password using the current password; an identity without a password needs a fresh passkey proof to set one.POST /users/me/email-change: newemailpluscurrentPasswordorpasskeyResponse; completion requires verification of the new address.DELETE /users/me: fresh password/passkey proof plusconfirmation: "delete-global-identity"; deletion has a recovery window.GET/POST /auth/passkeys, enrollment options and credential removal: passkey management. Step-up options provide the challenge for a fresh assertion.
Hosted forgot-password, reset, email-change and recovery screens retain validated application context. Email GETs show a confirmation page; their POSTs consume purpose-bound one-time tokens. The final active tenant administrator cannot disable their identity before appointing another active administrator. Active platform operators must first relinquish authority through approved administration.
Native /auth/register, /auth/login and social-provider APIs still serve the
legacy identity contract. Their tokens do not grant application tenant context.
The current hosted flow uses email/password; social callbacks remain separate
JSON APIs and require deliberate linking for an already registered email.
9. Errors and API reference#
Errors use { error: { code, message, details? } }. Handle:
| Status/code | Consumer behavior |
|---|---|
400 VALIDATION_ERROR |
Correct fields or malformed input |
401 UNAUTHORIZED |
Reauthenticate or renew appropriate proof; do not replay a rotated pair |
403 FORBIDDEN |
Missing key scope, role or authority |
409 ACCOUNT_SELECTION_REQUIRED |
Display the returned account memberships |
409 NO_ACCOUNTS |
Explicit signup or invitation path |
409 CONFLICT |
Resolve duplicate details, stale write or protected-admin constraint |
429 RATE_LIMITED |
Respect backoff; avoid automatic signup/email retry loops |
| Network/deadline failure | Fail closed; reconcile idempotent creation with the same request key |
Development, test, and staging expose Swagger at /docs and JSON OpenAPI at
/docs/json. Documentation routes are disabled in production. Each protected
operation identifies its identity, tenant, platform, service or application-key
requirement. Both
credentials are required together for POST /tenants. OIDC discovery is the
protocol endpoint reference.
Local checks: npm run build, npm run lint, npm test, and
npm run test:oidc-smoke. Integration coverage includes real hosted signup,
verification, PKCE callback, automatic selection, invitation and session revocation,
plus cross-application isolation. Operational evidence belongs to the deferred
deployment phase.