Identity platform

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/register and tenant/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:

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.