Self-service client registration
Self-service client registration
Internal MVS apps can register an oauth_client over an API instead of asking
an operator to run oidc:register by hand. The endpoints live on the central
IdP at https://c-auth.mvscloud.com/api/auth/admin/*. There are two ways in:
an admin session (a logged-in user whose role contains admin) or a
scoped Initial Access Token (IAT) an admin mints for a team or CI. There is
never an anonymous path.
This is for internal MVS apps only. The endpoints write the same
oauth_client row shape the OIDC authorize/token flow reads — registering
this way is equivalent to the oidc:register CLI, just over HTTP. The Better
Auth plugin’s own RFC 7591 /oauth2/register stays disabled.
The endpoints return 404 until an operator sets
OAUTH_SELF_REGISTRATION_ENABLED=true on the central-auth project. If you get
a 404, the feature is off — ask the operator, don’t change your request.
1. Mint an Initial Access Token (admin)
An admin mints an IAT and hands the one-time plaintext to the team or CI that will register. The token is bounded — it can only mint clients within the scopes, redirect prefixes, use-count, and TTL set here.
Response (the token is shown once — only a SHA-256 hash is stored):
Minting an IAT is admin session only — there is no IAT-mints-IAT path. The
bearer (IAT) path is rejected on registration-tokens.
2. Register a client
Register with the IAT in the Authorization header. Bearer credentials aren’t
auto-attached by browsers, so the IAT path is exempt from the cross-site origin
(CSRF) check that guards the cookie path. (An admin can also register directly
with a session cookie and a wider scope allowlist — same endpoint, no bearer.)
Response (the clientSecret is shown once):
action is "create" for a new client (HTTP 201) or "update" for an
existing one (HTTP 200). When Infisical auto-provision isn’t configured (see
§3), the infisical block is instead
{ "written": false, "error": "infisical_provision_not_configured" } — the
registration still succeeds and still returns the one-time secret.
3. What lands in Infisical
When you pass infisicalTarget, the service writes the credentials to that
Infisical path using a dedicated, least-privilege machine identity (distinct
from the app’s own runtime identity). The existing Infisical→Vercel sync then
delivers them to your project.
With idPrefix: "MVS_OIDC_" you get MVS_OIDC_CLIENT_ID /
MVS_OIDC_CLIENT_SECRET — exactly the env names the
OIDC integration guide expects. The write is idempotent
(create-or-update) and the target folder is created if it doesn’t exist.
Auto-provision requires the operator to have configured
INFISICAL_PROVISION_CLIENT_ID / INFISICAL_PROVISION_CLIENT_SECRET on the
central-auth project and for the request to include an infisicalTarget.
Without those provisioning credentials the write is a no-op: the response
carries infisical.written: false with
error: "infisical_provision_not_configured" (and omitting infisicalTarget
skips Infisical entirely).
Infisical writes are non-fatal. If the write fails, registration still
succeeds and returns the one-time secret — the response carries
infisical.written: false with an error, and the failure is audited. Save
the returned secret regardless; it cannot be retrieved later.
4. Security model
An exhausted IAT (usedCount >= maxUses), an expired one, or a revoked one
returns 401 invalid_token at validate time. A request whose scopes or
redirect URIs fall outside the token’s bounds returns 400 — the client is
not created. Distinct from both: a maxUses-limited IAT that passes
validation but loses the atomic consume race (a concurrent request consumed
the last use) returns 409 — also with no client created.
5. Lifecycle
Rotate a secret — re-register the same client (admin path) with secret
rotation; the new mvs_cs_… is returned once and an update that does not rotate
never clobbers the stored hash.
Disable a client — soft-disable (the token endpoint then rejects it; no hard delete):
List clients / tokens — admin session only; secrets and token hashes are never returned:
Revoke an IAT — sets revokedAt; the token can no longer register:
DELETE, GET (list), and minting tokens are admin session only — the
IAT bearer path is rejected on those. Only POST /api/auth/admin/oauth-clients accepts an IAT. Full request/response schemas
are in the Service Routes API reference under the Client Registration
tag.
6. Platform surface (c2-auth) — operator apps
Everything above is the central surface (c-auth.mvscloud.com), which serves
tenant apps and writes oauth_client rows keyed to User. There is a second,
fully isolated surface — platform (c2-auth.mvscloud.com) — that serves
operator/admin apps (e.g. mvs-pay, mvs-ops-ai, mvs-c2) and writes
platform_oauth_client rows keyed to PlatformUser. It is shipped and live in
production. The flow is identical to §1–§5 — same IAT-or-admin-session trust
gate, same one-time mvs_cs_… secret scheme, same idempotent-by-name behavior,
same infisicalTarget auto-provision — so this section states only the deltas.
Full operator-facing integration guide (endpoints, claims, Better Auth
genericOAuth config, token verification) lives in the
Operator-app OIDC integration page. Use c2-auth
when the relying party is an operator/admin tool; use c-auth (§1–§5) when it
serves tenant users.
Endpoints (platform)
Swap the host and base path. Everything under /api/auth/* on central maps to
/api/platform-auth/* on platform:
The redirect URI to register follows the Better Auth genericOAuth convention with
providerId: "c2-auth" — at better-auth ≥ 1.7 that is
{APP_BASE_URL}/api/auth/callback/c2-auth.
The two surfaces are isolated, and the boundary is enforced on every request.
A platform IAT (mvs_iat_… minted on c2-auth) presented to the central route
(/api/auth/admin/oauth-clients) is rejected 401 — and a central IAT presented
to the platform route is rejected 401 the same way. A client registered on
one surface is invisible to the other (platform_oauth_client / PlatformUser
vs oauth_client / User, with a surface-scoped registration token table).
Gating flag (platform)
The platform registration route is gated by PLATFORM_OAUTH_SELF_REGISTRATION_ENABLED=true
(the platform counterpart of central’s OAUTH_SELF_REGISTRATION_ENABLED), set in
Infisical /platform-auth on the mvs-platform-auth project — already on in
prod. It is a cold-start read, so toggling it requires a redeploy.
Operator CLI (platform)
For one-off registrations, the operator CLI mirrors the central oidc:register
command but writes a platform_oauth_client row. It is dry-run by default; add
--apply to write. Requires AUTH_DATABASE_URL.
Re-running with the same --name is idempotent (refuses to mint a duplicate
unless --force).