Identity Contract
Govern’s auth path is currently rooted in Microsoft Entra. The published Connector spec defines an IDP-neutral identity contract so external implementers using Okta, Auth0, Google Workspace, or in-house SSO can plug in without changing connector code. The reference implementation stays Entra-rooted; only the contract narrows to its IDP-neutral subset.
Field names like attorney_oid and entra_tenant_id stay intact in storage (no migration). Their meaning narrows: attorney_oid becomes “IDP-native stable subject ID”; entra_tenant_id becomes “IDP-specific tenant context, optional.”
Required claims
A compliant identity provider MUST mint access tokens with the following claims. Tokens missing any of these are rejected at the gateway with a structured invalid_token error.
| Claim | Type | Description |
|---|---|---|
sub | string | Stable IDP-native subject identifier for the human or service account. Persistent across sessions. Stored as attorney_oid in Govern internals. |
iss | string | Issuer URL. The runtime verifier checks this against the firm’s configured IDP issuer. |
aud | string | Audience. Must match the connector’s configured client identifier. |
iat | number | Issued-at, seconds since epoch. |
exp | number | Expiry, seconds since epoch. Govern rejects tokens with now > exp (with 60s clock-skew tolerance). |
firm_id | string | Govern firm-scope identifier. Issued by Govern at firm-bootstrap; injected into the token by the IDP via claim mapping. Tenant-isolation contract requires firm_id on every authenticated request. |
Optional IDP-specific claims
These are used opportunistically. Connectors that don’t surface them MUST NOT fail closed on their absence.
| Claim | Type | Description |
|---|---|---|
email | string | Display email. Surfaced in the audit feed and admin UI only. Not used for authorization. |
app_roles | string[] | Authorization roles. Govern checks for MCP.GovernAdmin / MCP.GovernReviewer to gate admin surfaces. Other roles are ignored. |
entra_tenant_id | string | The IDP-specific tenant context (tid for Entra, Okta org ID, Auth0 tenant, etc.). Stamped on every ledger row for tenant-isolation cross-checks. Other IDPs: mint an analogous claim and map it to this column. |
granted_scopes | string[] | OAuth scopes the token was minted with. Future scope-gated tools may narrow per-tool. |
idp_kind | string | Reserved — not yet minted. Discriminator for the IDP that issued this token (entra, okta, auth0, generic-oidc). Becomes the type-discriminator on IdentityClaims when the second IDP adapter ships. |
Token shapes
The Govern gateway accepts two token shapes:
- IDP-native access token — at OAuth/OIDC bridge endpoints. Verified against the IDP’s published JWKS. Used to bootstrap a Govern session.
- Govern-minted MCP token (HS256, signed with
JWT_SIGNING_KEYin Cloudflare secrets) — for in-session API calls. Carries the sameIdentityClaimsshape.
A connector implementing the identity port MUST handle both shapes — the OIDC verifier branch for IDP-native tokens, the symmetric verifier branch for Govern-minted MCP tokens.
Storage contract
The audit ledger schema preserves Entra-shaped column names for backward compatibility:
govern_events.attorney_oid— stores the IDP-nativesub. Stable per-subject across sessions, opaque to Govern.govern_events.entra_tenant_id— stores the IDP-specific tenant context. Tenant-isolation contract relies on this column being present and firm-scoped.govern_users.attorney_oid,govern_users.email,govern_users.app_roles— same semantics;emailandapp_rolesare display-only, refreshed per-token.
A connector implementing the contract for a non-Entra IDP MUST map its claims into this storage shape. Renaming the columns to idp_tenant_id was considered and rejected — the data-migration cost outweighs the cosmetic clarity.
Implementing a new IDP adapter
A connector adapter for a new IDP implements the identity contract by providing:
- JWKS / verification keys — typically fetched from
{issuer}/.well-known/openid-configurationand cached. - Issuer + audience config stored per-firm in
govern_firm_idp_config. - Claim mapper translating IDP-native claims to the required + optional set:
interface IdentityAdapter { verify(rawToken: string): Promise<IdentityClaims>; mintMcpToken(claims: IdentityClaims, ttlSeconds: number): Promise<string>; } - Tenant-context resolver mapping the IDP-specific tenant to the
entra_tenant_idstorage column. - (Optional) role-grant API — Microsoft Graph for Entra, Okta API for Okta. Used by
/admin/usersto grant/revoke Govern app-roles. Connectors without a role-grant API operate in roster-only mode.
Determinism boundary
Authorization decisions are deterministic. The identity contract is the input side of that boundary: claims feed into the policy engine, the engine emits a decision, the decision is logged. ML-based identity confidence scores are NOT load-bearing — if an IDP exposes such a signal, Govern may surface it as metadata but never gates on it.
The verify step is a pure boolean: token is valid (claims trusted) or invalid (request rejected). No “probably-valid” branch.
Versioning
The identity contract is versioned with the v1 Connector spec. Breaking changes to required claims, the storage contract, or the verify/mint signatures bump the spec major version. Adding a new optional claim is non-breaking. Adding a new required claim is breaking. Renames are breaking.
idp_kind is reserved but not yet minted. When the second IDP adapter ships, adding it as a discriminator on IdentityClaims is non-breaking — every Entra token gets idp_kind: ‘entra’ retroactively in claim normalization; no on-disk migration required.
Reference implementation
packages/adapters/src/entra-auth.ts— Entra token verification + JWKS handlingpackages/adapters/src/jwt-hs256.ts— Govern-minted MCP token mint + verifypackages/gateway/src/auth/middleware.ts— auth middleware that selects the verifier by token shapepackages/core/src/ports/auth-provider.ts— theAuthProviderport (validate/mint interface)