mcp entra oauth - Alshival-Ai/neural-labs GitHub Wiki
Future public mode: this implementation is retained for a later, separately reviewed public MCP release. V1 deliberately returns
404for public MCP and OAuth routes. Do not follow this runbook to enable ingress on a V1 host.
This runbook explains how Codex signs in with Microsoft and connects to the Streamable HTTP MCP endpoint in mcp/.
Microsoft Entra authenticates users and signs the access tokens. The MCP process is the protected resource server and also exposes a narrow OAuth metadata façade at /oauth/authorize and /oauth/token.
The retained managed-mode implementation lets the control plane own Entra
onboarding and send MCP only public tenant, client, scope, audience, authority,
and URL values through an internal bearer-authenticated endpoint. The current
Compose stack does not run this public server. Disabling MCP in the
administrator UI causes the MCP listener to return 503; re-enabling or
rotating public configuration is picked up without copying credentials between
containers.
The façade exists because Entra supports PKCE but its OpenID discovery document does not currently advertise code_challenge_methods_supported: ["S256"]; Codex requires that discovery field for MCP OAuth. The façade advertises OAuth 2.1 + PKCE, validates the exact client, loopback callback, resource, and least-privilege scope, and forwards authorization-code or refresh-token operations to the tenant-specific Entra endpoints. It does not issue tokens, retain sessions, accept client secrets, or use the application certificate.
For Entra, resource selection is expressed by the delegated scope rather than an RFC 8707 URL audience. The façade requires Codex's resource value to equal MCP_PUBLIC_URL, maps it to the fixed MCP_OAUTH_SCOPE, and the MCP resource server then verifies Microsoft's signature plus tenant, issuer, client, audience, expiry, and scp claim before invoking a tool.
Codex is a public OAuth client using authorization code with PKCE. It cannot safely hold an application secret or certificate. The certificate configured through the root .env authenticates the Neural Labs web application's confidential OIDC token exchange; it is never used or received by Codex or MCP.
Standalone MCP development can use the variables below. The managed Compose deployment obtains the corresponding public values from the control plane. Do not copy real values into Git, Compose files, command history, or tickets.
| Variable | Required | Meaning |
|---|---|---|
AZURE_CLIENT_ID |
yes | Entra application (client) ID. Also the default v2 access-token audience. |
AZURE_TENANT_ID |
yes | The only Entra tenant accepted by this single-tenant server. |
MCP_PUBLIC_URL |
yes | Canonical endpoint, including its path, such as https://mcp.example.com/mcp. Production must use HTTPS. |
AZURE_AUTHORITY_HOST |
no | Defaults to https://login.microsoftonline.com. Use an alternate Microsoft cloud authority only deliberately. |
MCP_OAUTH_SCOPE |
no | Full delegated scope requested from Entra. Defaults to api://$AZURE_CLIENT_ID/mcp.access. |
MCP_REQUIRED_SCOPE |
no | Value expected in the token's scp claim. Defaults to mcp.access. |
MCP_TOKEN_AUDIENCE |
no | Expected aud claim. Defaults to AZURE_CLIENT_ID, which is correct for Entra v2 access tokens issued to this API. |
MCP_HOST |
no | Listener address; defaults to 127.0.0.1. Use 0.0.0.0 only behind reviewed authenticated ingress or a container boundary. |
MCP_PORT |
no | Listener port; defaults to 3000. |
MCP_ALLOWED_HOSTS |
no | Comma-separated Host-header allowlist. Defaults to the public hostname plus local development hosts. |
MCP_ALLOWED_ORIGINS |
no | Optional comma-separated browser Origin-host allowlist. Non-browser MCP clients normally send no Origin header. |
Managed mode uses these additional values:
| Variable | Required | Meaning |
|---|---|---|
MCP_CONTROL_PLANE_CONFIG_URL |
yes | Internal control-plane config endpoint. HTTP is acceptable only on the private container network. |
MCP_CONFIG_TOKEN_FILE |
yes | File containing the shared high-entropy internal bearer token. |
MCP_CONFIG_POLL_INTERVAL_MS |
no | Reload interval from 5 seconds to 5 minutes; defaults to 30 seconds. |
Use the root .env.example only as a public field-name reference. Its identifiers and secrets are obvious placeholders.
The app registration must be single-tenant and serve as the Neural Labs web client, the API resource, and Codex's pre-registered public client. Follow the web sign-in setup for the web portion, then the future-only API steps below.
- Under Expose an API, set the Application ID URI to
api://<AZURE_CLIENT_ID>. - Add an enabled delegated permission named
mcp.access. Allow the intended tenant users or administrators to consent according to organizational policy. - Under Authentication, configure a Mobile and desktop applications public-client redirect URI. Do not guess it yet; Codex calculates a callback ID from the final MCP URL.
- Keep implicit grant disabled. The flow uses authorization code plus PKCE (
S256). - Do not add a client secret to Codex. The web credential remains encrypted in the control plane and is unrelated to Codex's public-client flow.
If different scope names or an Application ID URI other than api://<client-id> are chosen, set MCP_OAUTH_SCOPE, MCP_REQUIRED_SCOPE, and, when necessary, MCP_TOKEN_AUDIENCE to exactly match the registration.
cd mcp
npm ci
export MCP_PUBLIC_URL=http://127.0.0.1:3000/mcp
npm run devAZURE_CLIENT_ID and AZURE_TENANT_ID must already be in the environment. Plain HTTP is accepted only for loopback development. Check the public metadata without authenticating:
curl -fsS http://127.0.0.1:3000/healthz
curl -fsS http://127.0.0.1:3000/.well-known/oauth-protected-resource/mcpUse the final MCP URL because Codex derives the callback ID from the complete URL, including path and query string.
codex mcp add neural-labs \
--url "$MCP_PUBLIC_URL" \
--oauth-client-id "$AZURE_CLIENT_ID"Codex prints an OAuth callback URL. Register that exact URI in the Entra app's public-client redirect URI list. Entra does not currently advertise the issuer-bound authorization-response behavior required for Codex's shared callback, so expect a server-specific path similar to:
http://127.0.0.1/callback/<callback-id>
For an HTTP 127.0.0.1 redirect, the Entra portal may require editing the application manifest rather than the normal redirect-URI text box. Preserve the complete path. Codex inserts the temporary listener port during login.
After registering the callback:
codex mcp login neural-labs
codex mcp listComplete Microsoft sign-in in the browser. Then use the MCP whoami tool to verify the tenant and user identity returned by the validated access token.
- Put the exact HTTPS endpoint in
MCP_PUBLIC_URL; changing it changes Codex's callback ID. - Terminate TLS at reviewed ingress and forward the MCP endpoint, both OAuth façade endpoints, and both
/.well-known/discovery paths. Expose health only where operations require it. - Set an explicit Host allowlist and keep the process unprivileged.
- Load environment values from a protected service environment file or secret manager, not the repository.
- Confirm the Entra token is v2 and that its
audandscpvalues match this API. - Test unauthenticated requests return
401with aresource_metadatachallenge. - Test tokens from another tenant, another audience, an expired token, and a token without
mcp.accessare rejected. - Rotate Microsoft signing keys automatically through the advertised JWKS; never pin one signing certificate in this repository.
-
invalid_clientor redirect mismatch: reruncodex mcp addwith the final URL and register the exact callback it prints. -
AADSTS65001or consent errors: grant the delegated API permission to the user/client according to tenant policy. -
invalid_scope: compareMCP_OAUTH_SCOPEwith the full scope under Expose an API. - MCP
401 invalid_token: verify issuer, v2 token format, tenant ID, and audience; do not use a Microsoft Graph token for this API. - MCP
403 insufficient_scope: confirm the access token'sscpclaim containsMCP_REQUIRED_SCOPE, then re-runcodex mcp login neural-labs. - Metadata discovery failure: confirm
MCP_PUBLIC_URLis externally reachable and that/.well-known/oauth-protected-resource/<mcp-path>is not blocked by ingress.