Operator-app OIDC integration
Operator-app OIDC integration
The MVS platform OIDC provider is live in production at
https://c2-auth.mvscloud.com. It is the operator-facing IdP: use it when the
relying party is an MVS operator/admin tool (e.g. mvs-pay, mvs-ops-ai,
mvs-c2) whose users are operators — the PlatformUser directory, signed
in via Okta SSO + passkeys. Any standards-compliant OIDC client library works
(e.g. openid-client, Better Auth’s genericOAuth).
Two providers, two surfaces. Tenant apps consume central auth at
c-auth.mvscloud.com (see the OIDC integration guide);
operator apps consume platform auth at c2-auth.mvscloud.com. The two are
fully isolated: c2-auth FKs to PlatformUser and writes the
platform_oauth_* tables; c-auth FKs to User and writes oauth_client. A
client registered on one surface is invisible to the other. For the
shared-cookie redirect model (no OIDC), see the
Platform auth integration guide.
1. Endpoints
Resolve everything from the discovery document — let your client library derive the rest, and do not hardcode endpoints other than discovery.
The platform JWKS lives under /api/platform-auth/jwks — not
/api/auth/jwks (that path is central). Always resolve jwks_uri from
discovery rather than hardcoding it.
Invariants (enforced server-side, non-negotiable):
- PKCE S256 mandatory —
plainis rejected. - ID token signing is EdDSA (Ed25519) only — pin
algorithms: ["EdDSA"]in verifiers; never accept RS256/HS256. - Issuer is exactly
https://c2-auth.mvscloud.com(no trailing slash). - Dynamic client registration (RFC 7591) is disabled — every RP needs a
platform_oauth_clientrow (see §4).
2. Configure your client (Better Auth genericOAuth)
For an operator app running its own Better Auth instance and consuming c2-auth as an upstream OIDC provider:
Trigger sign-in from the client (Better Auth ≥ 1.7):
Callback / redirect URI
The redirect URI you register at c2-auth depends on your Better Auth version — this is the value the operator adds to your client’s exact-match allowlist:
The repo pins better-auth at 1.7.0-beta.5, so the ≥ 1.7 path applies:
register {APP_BASE_URL}/api/auth/callback/c2-auth.
Set your client env to the credentials returned at registration:
MVS_OIDC_CLIENT_ID and MVS_OIDC_CLIENT_SECRET (the mvs_cs_… secret is
shown once and stored hashed). If the app also consumes central auth — rare
for operator apps — use a distinct prefix (e.g. C2_AUTH_OIDC_CLIENT_*) to
disambiguate.
3. Claims
The c2-auth ID token and /userinfo response carry these operator claims,
sourced from PlatformUser (not tenant-org membership). sub, email,
name, etc. are standard.
Authorize on role / groups, never on email. These claims live on the ID
token and /userinfo — they are not on the access token. Consume them
from the ID token or userinfo.
4. Register your client
Dynamic registration is off; every RP needs a platform_oauth_client row. Both
paths below write that row through the one shared registration core
(surface: "platform") — identical validation, the mvs_cs_ secret scheme,
and idempotent-by-name upsert.
Path A — operator CLI
Dry-run by default; add --apply to write. Requires AUTH_DATABASE_URL
pointing at the auth database, run from the mvs-auth repo.
Flags mirror the central CLI: --scopes, --grant-types, --client-id,
--no-skip-consent, --rotate-secret, --force. Re-running with the same
--name is idempotent (it refuses to mint a duplicate unless --force).
Path B — self-service HTTP API
The admin registration route is gated by
PLATFORM_OAUTH_SELF_REGISTRATION_ENABLED=true (already on in prod). A platform
admin (a PlatformUser with an admin role) either calls it with an admin
session, or mints a platform Initial Access Token (IAT) to delegate scoped,
bounded registration.
Response (the clientSecret is shown once):
Platform IATs are minted at
POST https://c2-auth.mvscloud.com/api/platform-auth/admin/registration-tokens
(admin session only). The same request shape, IAT bounds, and Infisical
auto-provision (infisicalTarget) as central apply here — see the
self-service client registration guide.
The surfaces are isolated. A platform IAT can only mint platform clients —
presenting it to the central route (/api/auth/admin/oauth-clients) returns
401. Always register operator apps against c2-auth, not c-auth.
5. Verify tokens (resource servers)
ID tokens are EdDSA JWTs — verify against the platform JWKS, pinning the issuer,
your audience, and EdDSA only.
6. JWT access tokens (PLATFORM_OIDC_VALID_AUDIENCES)
By default the provider issues opaque access tokens (introspect them, or
just use the ID token). To receive an EdDSA JWT access token bound to your
API, send the RFC 8707 resource parameter on /authorize, and the operator
must add your API origin to the platform audience allowlist.
- Env var:
PLATFORM_OIDC_VALID_AUDIENCES(Infisical path/platform-auth, prod) — distinct from central’sOIDC_VALID_AUDIENCES. - Format: comma-separated absolute origins, exact match, no wildcards —
e.g.
https://pay.mvscloud.com/api,https://ops-ai.mvscloud.com/api. - Never set it to the issuer origin — self-aliasing rejects every RP. By design the parser never defaults to the issuer; unset means no aud-bound JWTs are accepted.
- Cold-start flag: changing it requires a redeploy of
mvs-platform-auth.
ID-token and opaque-token flows work without this var. Only set it when an operator RP needs aud-bound JWT access tokens, and only the operator can set it (it is not self-service).
7. Security model
The platform OIDC provider is shipped and live in production. Full request/response schemas are in the Platform Auth API reference.