MCP OAuth with OCI Identity Domains

This guide describes OAuth protection for the remote MCP server using OCI IAM Identity Domains. OAuth is optional. With no MCP_OAUTH_* settings, the MCP server continues to run without bearer-token protection. OAuth is enabled only when MCP_OAUTH_ENABLED=true.

There are three different roles to keep separate:

  • Resource server: the MCP API. It owns the audience and scopes and validates access tokens.

  • Authorization server: OCI IAM Identity Domains. It authenticates users, issues tokens, and manages grants.

  • OAuth client: the application or MCP client that requests a token and calls the MCP API.

OCI supports configuring one confidential application as both a resource server and an OAuth client. That is a valid option for this deployment. It does not make the MCP server the authorization server: OCI IAM remains the authorization server.

The recommended resource-server scope values are:

  • read — required for every MCP tool call.

  • update — required only for the reload operation.

Depending on the OCI IAM application configuration, the issued token may contain either the configured scope value or a fully qualified scope (FQS). The MCP server must validate the value actually present in the token. In this deployment, the issued token contains the configured values directly:

  • read — required for every MCP tool call.

  • update — required only for the reload operation.

Set MCP_OAUTH_REQUIRED_SCOPES and MCP_OAUTH_UPDATE_SCOPE to the exact values in the JWT scope claim.

When the authorization server requires a different scope spelling in the authorization request, set MCP_OAUTH_AUTHORIZATION_SCOPES to that request value. For OCI Identity Domains with primary audience oci-policy-analysis-mcp, use MCP_OAUTH_REQUIRED_SCOPES=read to validate the JWT and MCP_OAUTH_AUTHORIZATION_SCOPES=oci-policy-analysis-mcpread to advertise the request scope to Codex and other MCP clients.

The token request may still require the fully qualified scope. With primary audience oci-policy-analysis-mcp and configured scope value read, request oci-policy-analysis-mcpread; OCI IAM then issues a token whose scope claim is read.

If a different application configuration emits a fully qualified scope, use that exact value instead.

The MCP server validates the token signature, issuer, audience, and read scope. OAuth identifies and authorizes the caller; the MCP server still reads OCI using its configured profile, instance principal, resource principal, or session token.

OCI Identity Domain issuer/metadata-host compatibility

There are two unrelated OCI OAuth details that are easy to conflate:

  1. Scope translation is expected for this deployment. Codex requests oci-policy-analysis-mcpread, while OCI issues a JWT with scope=read. MCP_OAUTH_AUTHORIZATION_SCOPES and MCP_OAUTH_REQUIRED_SCOPES handle those two values respectively.

  2. Authorization-server identity is a client compatibility concern. The tenant-specific Identity Domain discovery endpoint is hosted at https://idcs-<id>.identity.oraclecloud.com/, but it can declare the canonical JWT issuer https://identity.oraclecloud.com/. A strict OAuth client can reject this metadata-host/issuer mismatch before opening the browser, even when the client ID, scopes, and redirect URI are correct.

Do not set MCP_OAUTH_ISSUER to the tenant-specific host merely to make those strings match. MCP_OAUTH_ISSUER validates the token’s actual iss claim and must remain the issuer OCI emits.

If Codex reports No authorization support detected after it successfully finds the protected-resource metadata, check the Identity Domain metadata first. Possible remediation paths are:

  • use an OCI Identity Domain endpoint/custom domain whose discovery URL and declared issuer match exactly, if OCI supports that for the domain;

  • add an MCP-side authorization-server metadata compatibility endpoint that has a stable matching issuer while retaining OCI’s actual authorize/token endpoints and JWT validation; or

  • use the explicit bearer-token configuration temporarily while the interactive compatibility path is being completed.

Codex OAuth with the optional OCI metadata shim

The optional shim is a static discovery document hosted by the MCP service. It does not proxy the browser, authorization code, token exchange, user credentials, client secret, or JWT validation. Codex still calls OCI IAM Identity Domains directly for /authorize and /token.

Enable it only when a client rejects the tenant-specific metadata host and canonical OCI issuer as different identities. Configure these additional container variables:

# Stable local logical issuer, served by the MCP application.
export MCP_OAUTH_AUTHORIZATION_SERVER_METADATA_ISSUER="https://<mcp-public-host>/oauth/oci-idcs"

# OCI IAM endpoints; Codex calls these directly.
export MCP_OAUTH_AUTHORIZATION_ENDPOINT="https://idcs-<id>.identity.oraclecloud.com/oauth2/v1/authorize"
export MCP_OAUTH_TOKEN_ENDPOINT="https://idcs-<id>.identity.oraclecloud.com/oauth2/v1/token"

With those values, the service advertises the local logical issuer in protected resource metadata and serves its OAuth metadata at both:

/.well-known/oauth-authorization-server/oauth/oci-idcs
/oauth/oci-idcs/.well-known/oauth-authorization-server

The document has a matching local issuer, but its authorization_endpoint and token_endpoint remain the OCI Identity Domain URLs. Keep the separate MCP_OAUTH_ISSUER=https://identity.oraclecloud.com/ value unchanged: that is for validating OCI’s issued JWTs, not for client discovery.

Identity Domain application checklist

The deploy script can pass environment variables and create the container, but these OAuth-client controls remain a deliberate Identity Domain administrator action:

  1. Enable Authorization Code for the application used by Codex.

  2. Enable PKCE with S256 and permit public-client token exchange (none), because Codex does not provide an OCI client secret.

  3. Grant the application the MCP resource scope (read; OCI may require its fully-qualified request spelling oci-policy-analysis-mcpread).

  4. Assign the intended user/group access to the application.

  5. Do not register the LB’s /mcp URL as a redirect URI. It is the resource server, not Codex’s callback receiver.

  6. After the first successful codex mcp login begins, copy the full derived loopback redirect URI that Codex sends in redirect_uri and add it exactly to the Integrated Application. Codex appends a server-specific suffix to the configured base callback URL.

For a local workstation, configure the Codex global callback base once:

mcp_oauth_callback_url = "http://127.0.0.1:9999/oauth/callback"
mcp_oauth_callback_port = 9999
mcp_oauth_credentials_store = "auto"

Then add or enable the Streamable HTTP MCP entry, set its OAuth client ID and resource URL, and run:

codex mcp login <server-name> --scopes oci-policy-analysis-mcpread

Deployment order

  1. Create the OCIR repository and build/push an image with local-build-push-mcp-ocir.sh.

  2. Configure the Identity Domain resource server/client controls above.

  3. Configure the HTTPS LB, certificate, backend health check, and NSGs.

  4. Run local-deploy-mcp-container-instance.sh with the regular OAuth values plus the three shim variables when needed.

  5. Verify /health, protected-resource metadata, and authorization-server metadata through the LB before enabling the Codex entry.

  6. Run codex mcp login, register its derived callback URI in the Identity Domain app, and repeat login to complete the authorization-code flow.

1. App Creation: resource server, OAuth client, and scopes

Create one Confidential Application in the target OCI IAM Identity Domain. The same application may be configured in both sections below.

Resource-server configuration

  1. Open the Identity Domain and select Integrated applications.

  2. Select Add application → Confidential Application → Launch workflow.

  3. Give the application a name such as OCI Policy Analysis MCP.

  4. In OAuth configuration, enable Configure this application as a resource server now.

  5. Set the Primary audience to a stable value such as oci-policy-analysis-mcp. This becomes MCP_OAUTH_AUDIENCE; it is not necessarily the client ID.

  6. Add these scopes:

    read
    update
    

The resource-server application defines what access to MCP means. A client must later be granted these scopes before OCI IAM will issue a token that contains them.

Granting access to a group of users

For Authorization Code access, assign the permitted Identity Domain group to the OAuth client application. Users in that group can then authenticate through the application and receive its allowed resource scope. The MCP server validates the resulting token; it does not query Identity Domain group membership during each request.

If the server must enforce a group independently of application assignment, configure an OCI IAM custom token claim containing group membership and add application-level authorization for that claim. Scope validation alone does not distinguish one user group from another.

Client configuration

If this same application will request tokens, also enable Configure this application as a client now and select only the grant types needed by the deployment:

  • Client Credentials for a public/service-to-service integration.

  • Authorization Code for a browser-based three-legged integration.

  • Refresh Token if long-lived user sessions need token renewal.

  • Resource Owner only for a tightly trusted integration that explicitly collects the user’s credentials. Authorization Code is preferred for interactive users because user credentials are not exposed to the client.

For a separate caller application, configure that application as a client and grant it the MCP resource scopes instead. The resource-server app and caller app do not have to be the same application.

For Authorization Code, register the caller’s exact HTTPS redirect URL in the client application. The redirect URL belongs to the OAuth client handling the browser flow, not to the MCP bearer-token validation endpoint.

Activate the application after saving the configuration. Record:

  • Identity Domain issuer URL, matching the token iss claim exactly.

  • Signing certificate JWKS URL, normally ending in /admin/v1/SigningCert/jwk.

  • Resource-server primary audience.

  • Authorization server URL.

  • Client ID and client secret, if the application is configured as a confidential client.

  • The two resource scopes.

See Oracle’s Register the Application in OCI IAM and Configuring OAuth documentation.

Start the MCP server

Set the OAuth resource-server settings on the container instance and start or restart the existing MCP container:

export MCP_OAUTH_ENABLED="true"
export MCP_OAUTH_ISSUER="https://idcs-<id>.identity.oraclecloud.com/"
export MCP_OAUTH_JWKS_URI="https://idcs-<id>.identity.oraclecloud.com/admin/v1/SigningCert/jwk"
export MCP_OAUTH_AUDIENCE="oci-policy-analysis-mcp"
export MCP_OAUTH_REQUIRED_SCOPES="read"
export MCP_OAUTH_UPDATE_SCOPE="update"
export MCP_OAUTH_AUTHORIZATION_SCOPES="oci-policy-analysis-mcpread"
export MCP_OAUTH_RESOURCE_SERVER_URL="https://<mcp-public-host>"
export MCP_OAUTH_AUTHORIZATION_SERVER_URL="https://idcs-<id>.identity.oraclecloud.com/"
export MCP_OAUTH_ALGORITHM="RS256"

The deployment script or container entrypoint then starts the server with the configured transport, host, port, and OCI authentication mode. If MCP_OAUTH_ENABLED=true and a required value is missing, startup fails before loading tenancy data and prints a setup error.

MCP_OAUTH_REQUIRED_SCOPES is space- or comma-separated and applies to every MCP tool. MCP_OAUTH_UPDATE_SCOPE is checked separately for reload.

2. Connecting with a Bearer Token

The MCP server is a protected resource. A caller presents an access token in the HTTP request:

curl -i \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Accept: application/json, text/event-stream" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}' \
  https://<mcp-public-host>/mcp

The JWT should contain:

  • iss equal to MCP_OAUTH_ISSUER.

  • aud containing MCP_OAUTH_AUDIENCE.

  • scope containing read.

  • update only for callers allowed to reload.

Expected behavior:

  • No bearer token: MCP access is rejected.

  • Read scope: normal search and analysis tools work.

  • Read without update scope: reload is rejected.

  • Read plus update scope: reload is authorized, subject to the server’s OCI authentication mode and loaded-data requirements.

Start the MCP server

Use the same environment block from App Creation, then restart the container. No callback is required for this bearer-token connection: the caller obtains the token, and the MCP server only validates it.

3. Two-legged service integration: Client Credentials

Client Credentials is the two-legged machine-to-machine flow. In this section, “public” means an externally reachable service integration; it does not mean an OAuth public client. Client Credentials requires the client to protect its credentials. There is no resource owner signing in through a browser and no callback. The OAuth client authenticates to OCI IAM with its client ID and secret and requests the MCP resource scope.

If using the same app as both resource server and client, grant that app its own MCP scopes under its client Resources configuration. A separate client application can be granted the same scopes instead.

Example token request:

export BASIC_AUTH="$(printf '%s:%s' "$CLIENT_ID" "$CLIENT_SECRET" | base64)"

curl -sS \
  -H "Authorization: Basic $BASIC_AUTH" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials&scope=oci-policy-analysis-mcpread" \
  "https://idcs-<id>.identity.oraclecloud.com/oauth2/v1/token"

Request oci-policy-analysis-mcpupdate only for a service that is authorized to invoke reload. Use the returned access_token as $ACCESS_TOKEN in the Bearer Token example. The resulting JWT scope is still validated as update.

Start the MCP server

Start the MCP server with the resource-server environment block from App Creation. The server does not need the client ID or client secret; those remain with the calling service. The server validates the resulting JWT against the issuer, JWKS endpoint, audience, and scopes.

See Oracle’s Client Credentials Grant Type documentation.

4. Three-legged integration: Authorization Code and callback

Authorization Code is the three-legged flow: OAuth client, resource owner, and OCI IAM. The user authenticates at OCI IAM, grants access, and OCI IAM redirects the browser back to the OAuth client with an authorization code. The client exchanges that code for an access token and then calls MCP with the bearer token.

The callback must be implemented by the component acting as the OAuth client. It does not need to be implemented by the MCP resource server when an external MCP client or gateway owns the login flow. In that common arrangement:

  1. Register the external client’s exact HTTPS callback URL in OCI IAM.

  2. The client redirects the browser to the Identity Domain authorization endpoint with client_id, redirect_uri, response_type=code, scope, state, and a PKCE challenge where supported.

  3. OCI IAM redirects back to the client callback with code and state.

  4. The client validates state and exchanges the code at the token endpoint.

  5. The client sends the access token to MCP using the Bearer Token example.

If the MCP server itself is intended to be that OAuth client, then the server needs additional application behavior that it does not currently provide:

  • a login endpoint that creates and stores state and PKCE values;

  • a registered callback route;

  • code-to-token exchange with OCI IAM;

  • secure storage and refresh of the resulting token; and

  • a way to associate the authenticated user/token with MCP sessions.

RemoteAuthProvider currently configures MCP as a protected resource and advertises the authorization server; it does not implement that client-side callback flow. The simplest three-legged design is therefore to let the MCP client or an OAuth gateway handle the callback and pass the resulting bearer token to this server.

Manual three-legged smoke test

For a temporary test, you can perform the browser step manually without a callback server. Register a redirect URI that you can use for testing, such as http://127.0.0.1:8765/oauth/callback. After login, the browser may show a connection error because no callback route exists; copy the code and state from the resulting URL.

Set the client and redirect values:

export IDCS_BASE_URL="https://idcs-<id>.identity.oraclecloud.com"
export IDCS_CLIENT_ID="<authorization-code-client-id>"
export IDCS_CLIENT_SECRET="<authorization-code-client-secret>"
export REDIRECT_URI="http://127.0.0.1:8765/oauth/callback"

Generate the authorization URL. The token request below uses the fully qualified OCI scope oci-policy-analysis-mcpread; the issued JWT is expected to contain scope=read.

python - <<'PY'
import os
import secrets
import urllib.parse

params = {
    "client_id": os.environ["IDCS_CLIENT_ID"],
    "response_type": "code",
    "redirect_uri": os.environ["REDIRECT_URI"],
    "scope": "oci-policy-analysis-mcpread",
    "state": secrets.token_urlsafe(24),
}
print(os.environ["IDCS_BASE_URL"] + "/oauth2/v1/authorize?" + urllib.parse.urlencode(params))
PY

Open the printed URL in a browser, sign in as an allowed user, and copy the code and state query parameters from the redirect URL. Verify the returned state matches the state printed in the authorization URL, then exchange the one-time code:

read -r AUTH_CODE

export ACCESS_TOKEN="$(curl -sS \
  -u "$IDCS_CLIENT_ID:$IDCS_CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=authorization_code" \
  --data-urlencode "code=$AUTH_CODE" \
  --data-urlencode "redirect_uri=$REDIRECT_URI" \
  "$IDCS_BASE_URL/oauth2/v1/token" | jq -r '.access_token')"

Initialize the MCP session and save its session ID:

export MCP_URL="http://127.0.0.1:8765/mcp"

curl -sS \
  -D /tmp/mcp-init.headers \
  -o /tmp/mcp-init.body \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Accept: application/json, text/event-stream" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}' \
  "$MCP_URL"

cat /tmp/mcp-init.body
export MCP_SESSION_ID="$(awk 'tolower($1)=="mcp-session-id:" {print $2}' /tmp/mcp-init.headers | tr -d '\r')"

Complete the MCP handshake:

curl -sS \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Mcp-Session-Id: $MCP_SESSION_ID" \
  -H "Accept: application/json, text/event-stream" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}' \
  "$MCP_URL"

Invoke an MCP tool:

curl -i \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Mcp-Session-Id: $MCP_SESSION_ID" \
  -H "Accept: application/json, text/event-stream" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"policy_search","arguments":{}}}' \
  "$MCP_URL"

At MCP_LOG_LEVEL=WARNING, the server should log the authenticated user identity and scopes for the tool invocation. This manual flow is for testing; production use should keep state, authorization codes, client secrets, and access tokens inside a real callback/session service.

Start the MCP server

Start the MCP server with the same resource-server environment block. If an external client owns the callback, no additional MCP callback setting is needed. If MCP is later expanded to own the callback, the callback URL must be added to the client configuration and exposed through the MCP deployment’s HTTPS ingress.

See Oracle’s Authorization Code Grant Type documentation.

Diagnostics and troubleshooting

At MCP_LOG_LEVEL=WARNING, each MCP tool invocation logs the caller and granted scopes without logging the bearer token or client secret:

MCP OAuth invoke policy_search client_id=<caller> user_displayname=<user> ca_name=<account> scopes=<granted-scopes>

OCI IAM commonly supplies the human-readable fields as user_displayname and ca_name; the fallback client_id is usually the token subject (sub). These identity fields are useful for audit diagnostics but are still user data. Keep the warning logs access-controlled and avoid enabling request-input logging at INFO in production unless policy text and request filters are acceptable to store in logs.

Common failures:

  • iss mismatch: copy the exact iss value from the JWT into MCP_OAUTH_ISSUER.

  • aud mismatch: use the resource server’s primary audience, not the client ID unless they are intentionally identical.

  • Missing scope: grant the client access to the MCP resource scope and request the fully qualified scope from the token endpoint. Configure MCP_OAUTH_REQUIRED_SCOPES and MCP_OAUTH_UPDATE_SCOPE with the exact value present in the JWT scope claim.

  • JWKS failure: verify that the JWKS endpoint is reachable anonymously by the container.

  • Reload denied: ensure the token contains the configured MCP_OAUTH_UPDATE_SCOPE value.

  • Callback mismatch: ensure the redirect_uri exactly matches the URL registered on the OAuth client, including scheme, host, path, and port.