External IdP integration recipes (mode C / federated mode B)¶
Per-IdP configuration recipes for connecting Précis-MCP to Auth0, Okta, Microsoft Entra ID, and Ping as the OIDC issuer, plus the DCR support matrix that decides which identity mode fits which IdP. Companion to Remote access — sign-in & identity modes.
Mode C (direct external OIDC trust) is fully supported on the Précis-MCP side — issuer/JWKS/audience override, claim→column mapping, pre-registered client, and the boot-time conformance check (
PRECIS_AUTH_PREFLIGHT). These recipes describe what an operator must configure on the IdP side to satisfy the token contract.Verification caveat: compiled from official vendor documentation (cited inline). IdP consoles change, and some behaviours rest on concept/guide pages or inference rather than a normative reference — where that's the case it is flagged. Validate the exact field labels and the RFC 8707 behaviours against a live tenant before committing an architecture.
1. How to choose (read first)¶
Your IdP's anonymous-DCR posture decides whether mode C is viable for the public connectors (claude.ai / ChatGPT), or whether you need mode B (Keycloak brokering). Only Auth0 (after enabling it) and PingFederate support anonymous RFC 7591 Dynamic Client Registration. Okta, Entra ID, and PingOne cloud SaaS do not. Since claude.ai/ChatGPT self-register via DCR, an Okta/Entra/PingOne estate cannot point those connectors at mode C — the path is mode B, where the bundled Keycloak supplies the DCR surface and the RFC 8707 audience stamping while federating authentication upstream (brokering walkthrough).
Entra and Okta are the two most common enterprise IdPs, so in practice most deployments land on mode B (brokered), not mode C. Mode C is the clean path for Auth0 and PingFederate, and for first-party frontends / pre-registered clients on any IdP (which don't need DCR).
A second gap compounds it: Okta and Entra do not honour RFC 8707 resource
indicators (Entra rejects resource with AADSTS901002; Okta's aud is a
static per-authorization-server setting). So even the no-DCR mode-C path (SPA /
pre-registered client) against Okta/Entra needs either a proxy that rewrites
resource→scope, a relaxed audience check, or — again — mode B, where Keycloak
stamps the aud.
2. Cross-IdP summary matrix¶
| Dimension | Auth0 | Okta | Entra ID | PingFederate | PingOne (cloud) |
|---|---|---|---|---|---|
| Custom access-token claim | Post-login Action setCustomClaim; must be namespaced (https://precis/…) on API-audience tokens |
Custom Authorization Server only (org server can't); EL expression; API Access Mgmt paid add-on | Optional claims / directory-extension / claims-mapping policy; shape the resource app's token, not the client's | ATM attribute contract + contract fulfillment + OGNL (most flexible) | Resource custom attributes + scopes; can't touch built-in resources |
| Stable join key | any namespaced claim value (use a stored internal id) | EL over a UD attribute (user.login / custom) |
oid (+tid) — never email/upn (mutable); sub is pairwise |
datastore/adapter attribute via OGNL | resource attribute → user id |
aud source |
registered API Identifier | custom AS Audience field (static) | API client-ID GUID (v2) / App ID URI (v1) | Resource URIs on the ATM | resource identifier |
RFC 8707 resource |
only with Resource Parameter Compatibility Profile on | No (static per-AS aud) | No (AADSTS901002; scope/.default model) |
Yes (native, pre-register the URI) | Not documented |
| Anonymous DCR (RFC 7591) | Yes, but off by default + operational prereqs | No (needs SSWS/okta.clients.manage) |
No (Graph/portal only) | Yes (/as/clients.oauth2, optional IAT + policies) |
No (Worker-token API) |
| Discovery / JWKS / RS256 | Yes | Yes | Yes (issuer quirk, below) | Yes (use a JWT ATM, not reference) | Yes |
| Keycloak can broker to it (mode B) | Yes (OIDC) | Yes (OIDC) | Yes (built-in MicrosoftIdentityProvider, OIDC/SAML) |
Yes (OIDC/SAML) | Yes (OIDC) |
| Recommended Précis-MCP posture | Mode C viable (DCR + 8707 both configurable) | Mode B for public connectors; mode C only for pre-registered clients + proxy/relaxed aud | Mode B for public connectors; mode C needs scope-rewrite + oid join |
Mode C viable (DCR + 8707 both native) | Mode B (no DCR, no 8707) |
3. Per-IdP recipes¶
Each recipe has three parts: what to configure in the IdP, the matching
deploy/.env block on the Précis-MCP side (mode C — set
PRECIS_AUTH_MODE=oidc and drop bundled-keycloak from COMPOSE_PROFILES),
and verification. Verification is the same everywhere:
python -m precis_mcp.admin_cli check-auth # issuer / JWKS / audience reachability
then set PRECIS_AUTH_PREFLIGHT=1 so the server fails fast at boot on a
misconfiguration, and confirm end to end: a client signs in, a query succeeds,
and a token with the wrong /mcp audience is rejected.
3.1 Auth0 — mode C viable¶
Anonymous DCR and RFC 8707 are both available, so Auth0 carries the full mode-C surface, public connectors included.
Configure in Auth0:
- Register the API (Applications → APIs). Its Identifier — an
absolute URI that is never dereferenced — becomes the token
aud. Using your/mcpURL keeps it self-describing. - Emit the identity claim with a post-login Action:
api.accessToken.setCustomClaim('https://precis/precis_user_id', event.user.app_metadata.precis_user_id). Namespacing is mandatory on API-audience tokens — a non-namespaced claim is silently dropped; the namespace URL is opaque. Store each user's Précis-MCP id inapp_metadataat provisioning. You cannot overwritesub/aud— emit a dedicated claim and have the verifier read it. - Enable RFC 8707 via the Resource Parameter Compatibility Profile
so Auth0 honours
resource=<identifier>; otherwise clients must send the legacyaudience=parameter (if both are present,audiencewins). One token targets one API audience. - For the public connectors, enable anonymous DCR
(
enable_dynamic_client_registrationin tenant settings — off by default; the endpoint is/oidc/register). DCR clients are third-party (tpc_), receive onlyauthorization_code+refresh+ PKCE, and need a domain-level (promoted) connection plus a pre-set API grant before they can obtain tokens. Registration has no per-client gating — monitor and rate-limit the endpoint. (Auth0 is steering future MCP registration toward CIMD.) - Pre-register a client for any first-party frontend or static client; note its client id (and secret, if confidential).
Précis-MCP side:
PRECIS_AUTH_MODE=oidc
OIDC_ISSUER=https://<tenant>.auth0.com/ # keep the trailing slash — used verbatim
OIDC_JWKS_URL=https://<tenant>.auth0.com/.well-known/jwks.json
OIDC_AUDIENCE=<api-identifier> # the API Identifier from step 1
OIDC_CLIENT_ID=<pre-registered-client-id> # for static clients; DCR clients bring their own
PRECIS_IDENTITY_CLAIM=https://precis/precis_user_id
# PRECIS_IDENTITY_COLUMN=id (default): the claim carries the platform user id itself
Discovery: https://{tenant}.auth0.com/.well-known/openid-configuration;
JWKS at /.well-known/jwks.json; RS256 default.
Sources: create-custom-claims, resource-param-compatibility-profile, dynamic-client-registration.
3.2 Okta — mode B for public connectors¶
No anonymous DCR and no RFC 8707: claude.ai/ChatGPT cannot connect to an Okta-backed mode C — route those users through the bundled Keycloak brokered to Okta. The recipe below is the mode-C path for pre-registered clients (a first-party frontend, Claude Code with a configured client id).
Configure in Okta:
- Create (or pick) a Custom Authorization Server (Security → API → Authorization Servers) — the org/default server cannot emit custom claims on access tokens. Licensing: custom authorization servers require the API Access Management add-on — paid in production, pre-provisioned in free/trial orgs, which hides the cost during evaluation.
- Set the audience: the server's Audience field is the static
audof every token it issues. There is no RFC 8707 — the audience is per-server, selected by which/oauth2/<id>/v1/…endpoint the client uses, not by aresourceparameter. One custom AS per resource audience is the workaround. - Add the identity claim (custom AS → Claims → Add): name
precis_user_id, token type Access Token, value type Expression — e.g.user.loginor a custom UD attribute. - Pre-register the client (Applications). DCR
(
/oauth2/v1/clients) requires an org credential (SSWS token orokta.clients.manage) — public connectors cannot self-register.
Précis-MCP side:
PRECIS_AUTH_MODE=oidc
OIDC_ISSUER=https://<org>.okta.com/oauth2/<authServerId>
OIDC_JWKS_URL=https://<org>.okta.com/oauth2/<authServerId>/v1/keys
OIDC_AUDIENCE=<custom-AS-audience> # the static Audience from step 2
OIDC_CLIENT_ID=<pre-registered-client-id>
PRECIS_IDENTITY_CLAIM=precis_user_id # the claim name from step 3
PRECIS_IDENTITY_COLUMN=external_id # store user.login (or your UD attribute) per user
Discovery/JWKS/RS256: yes, per authorization server. (The RFC 8707 "no" is
inferred from Okta's static-audience model — no Okta doc claims resource
support.)
Sources: customize-tokens, customize-authz-server, API Access Management, DCR API.
3.3 Microsoft Entra ID — mode B for public connectors¶
No DCR at all (app registration is portal/Graph only) and no RFC 8707
(resource yields AADSTS901002): public connectors cannot connect to an
Entra-backed mode C — route them through the
bundled Keycloak brokered to Entra. The recipe below
is the mode-C path for pre-registered clients.
Configure in Entra:
- Register the API app (the resource): Expose an API → set the
Application ID URI (
api://<client-id>) and add a scope. SetrequestedAccessTokenVersion: 2in the app manifest so v2 tokens are issued; theaudof a v2 token is the API app's client-ID GUID (v1 uses the App ID URI instead). - Register the client app (the caller) with the redirect URIs your
client needs; grant it the API scope from step 1. Clients request
scope=api://<id>/<scope>(or/.default) — neverresource=. - Use
oid(+tid) as the join key — never email/UPN.oidis the immutable per-tenant user GUID;email/upn/preferred_usernameare mutable and reassignable;subis pairwise (per-app, useless as a cross-system key). Verifytidequals your tenant — the sameoidin two tenants is two different users.oidis present in v2 access tokens by default, so no claims customisation is needed for this path. - (Only if you need a custom claim instead of
oid) — access tokens are shaped by the resource app registration, not the client ("to validate access-token changes, request a token for your application, not Graph"). Add a directory-extension claim (extn.precisUserId) or a claims-mapping policy (acceptMappedClaims: true, single-tenant only — never set it on a multitenant app).
Précis-MCP side:
PRECIS_AUTH_MODE=oidc
OIDC_ISSUER=https://login.microsoftonline.com/<tenant-id>/v2.0
OIDC_JWKS_URL=https://login.microsoftonline.com/<tenant-id>/discovery/v2.0/keys
OIDC_AUDIENCE=<api-app-client-id-guid> # v2 tokens
OIDC_CLIENT_ID=<client-app-id>
PRECIS_IDENTITY_CLAIM=oid
PRECIS_IDENTITY_COLUMN=external_id # store each user's oid GUID
Discovery quirk: pin the verifier to the tenant-specific metadata
(https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration;
JWKS at /discovery/v2.0/keys) so iss is concrete — the common endpoint
returns the literal {tenantid} template.
Sources: optional-claims, access-tokens, expose-web-apis, claims-customization.
3.4 Ping — mode C viable on PingFederate only¶
Caveat: "Ping" spans three products with different OAuth stacks —
PingOne (cloud SaaS), PingFederate (on-prem), and PingOne Advanced
Identity Cloud / PingAM (ex-ForgeRock). The /oauth2/register "Allow Open
DCR" docs that surface under "PingOne" are usually AIC/PingAM, not PingOne
cloud SaaS. Ping docs are JS-rendered and harder to deep-link — validate
against a live tenant.
PingFederate (mode C viable — DCR and RFC 8707 both native):
- Create an Access Token Management (ATM) instance of type JWT
(not reference/opaque — the token must be JWKS-verifiable). Declare
precis_user_idin the attribute contract and bind it in Contract Fulfillment from an IdP adapter / datastore, with OGNL transforms where needed (the most flexible claim pipeline of the four vendors). - Pre-register your
/mcpURI in the ATM's Resource URIs — RFC 8707 is native: theresourcerequest parameter selects the ATM instance by matching those URIs (an unregistered URI errors). - DCR for public connectors: anonymous registration at
/as/clients.oauth2, configurable with or without an initial access token and constrained by Client Registration Policies.
PingOne cloud (mode B): custom claims via Resource → custom
Attributes (built-in openid/PingOne API resources can't be modified),
aud from the resource identifier, no anonymous DCR (Worker-token
Application API only), RFC 8707 not documented — route public connectors
through brokering.
Précis-MCP side (PingFederate):
PRECIS_AUTH_MODE=oidc
OIDC_ISSUER=https://<pingfederate-host>
OIDC_JWKS_URL=https://<pingfederate-host>/pf/JWKS
OIDC_AUDIENCE=<resource-uri-from-the-ATM>
OIDC_CLIENT_ID=<pre-registered-client-id> # for static clients; DCR clients bring their own
PRECIS_IDENTITY_CLAIM=precis_user_id
PRECIS_IDENTITY_COLUMN=external_id # or id, if the fulfilled value is the platform id
Sources: PF access-token mapping, PF dynamic registration, PingOne customize access token.
3.5 The pre-registered-client path (mode C, no DCR)¶
When the IdP has no anonymous DCR (Okta / Entra / PingOne) or the client is a first-party frontend, the OAuth client is registered manually in the IdP and its id configured into Précis-MCP + the client. This is the supported alternative to DCR:
- Register a client in the IdP — confidential (with a secret) or
public+PKCE. Note its
client_id(and secret if confidential). Set the redirect URI(s) for the SPA browser flow. - Tell Précis-MCP the pre-registered client — set
OIDC_CLIENT_ID(andOIDC_CLIENT_SECRETfor a confidential client) on the deployment. These override the bundled-Keycloak default (precis-spa). - Configure the MCP client / first-party frontend with that same
client_id. - Discovery already points the client at the IdP — the RFC 9728
protected-resource metadata advertises
authorization_servers = [issuer], so a modern client reaches the IdP's authorize/token endpoints directly. No registration call is made.
The Keycloak oauth_proxy shim is disabled in mode C (it builds
Keycloak-shaped endpoints and exists only for claude.ai against bundled
Keycloak). Modern clients (Claude Code, ChatGPT, a first-party frontend) use the
RFC 9728 discovery path above and never touch it.
claude.ai / ChatGPT remain DCR-only — they cannot use a pre-registered client_id, so against a no-DCR IdP they are unsupported in mode C; use mode B instead (Keycloak brokering supplies DCR).
3.5.1 Excel add-in against a no-DCR IdP (Entra / Okta direct)¶
The Excel add-in is a pre-registered public client, so the DCR gap that
blocks claude.ai/ChatGPT does not apply to it — it can connect to a no-DCR IdP
in mode C directly. It also handles the second gap (Okta/Entra not honouring the
RFC 8707 resource parameter) by reading its token-request shape from the
/mcp host's protected-resource metadata, so you configure the shape on the
server and the add-in follows it.
- Register a public client (Authorization Code + PKCE, no secret) in your
IdP for the add-in, separate from the SPA's client. Set its redirect URI to
the server-hosted add-in callback
(
https://<your-instance>/excel/auth-callback.html) and allow the hosted Précis origin (https://<your-instance>) wherever the IdP requires a web origin for browser PKCE token exchange. On Entra, register the callback under the Single-page application platform, not Web — the add-in redeems the code with a browserfetch, and Entra only enables cross-origin token redemption for SPA-typed redirect URIs (a Web-typed URI clears theAADSTS50011redirect-mismatch error but then fails withAADSTS9002326). - Point the
/mcphost at it — setEXCEL_ADDIN_CLIENT_IDto that client_id. - Advertise the token shape for an IdP that binds the audience via a scope
rather than
resource(Entra, Okta):
EXCEL_ADDIN_CLIENT_ID=<the public client_id>
EXCEL_ADDIN_SCOPE="openid profile email offline_access api://<api-app-id>/access_as_user"
EXCEL_ADDIN_RESOURCE_INDICATOR=false
The add-in then requests exactly that scope and omits resource. For an IdP
that does honour resource (Auth0 with the compatibility profile, Ping),
leave both knobs unset — the add-in keeps the default RFC 8707 shape.
4. The /mcp audience check is unchanged. The token's aud must still
match OIDC_AUDIENCE; with Entra that is the API app's client-ID GUID, set
by the api://…/access_as_user scope above.
Include offline_access in the scope so the add-in receives a refresh token —
Entra issues one only when that scope is explicitly requested — and profile
email so the id_token carries the user's name/email and the task pane shows the
signed-in user rather than the opaque subject id. The add-in keeps the token in
session memory only; it is never written to the workbook or disk.
4. Choosing the identity claim¶
Map PRECIS_IDENTITY_CLAIM / PRECIS_IDENTITY_COLUMN to the IdP's stable
identifier, which differs per IdP: Entra oid (and assert tid), Okta a
UD attribute emitted via a custom-AS claim, Auth0 a stored app_metadata id
emitted as a namespaced claim, Ping a datastore/adapter attribute. Email is
the wrong choice everywhere — it is mutable and reassignable. When the claim
value differs from your Précis-MCP user ids, set
PRECIS_IDENTITY_COLUMN=external_id and store the IdP value at provisioning
with create-user --external-id (see
Remote access).
5. Related documents¶
- Remote access — sign-in & identity modes — the three identity modes, the token contract, and the bundled-Keycloak setup these recipes plug into.
- Sign in with your corporate IdP (mode B, brokered) — the brokered-path walkthrough recommended above for Okta / Entra / PingOne deployments.
- Environment variable reference
— the
OIDC_*/PRECIS_IDENTITY_*configuration surface (mode C).