# Native OAuth connection to CiteDoor This runnable example uses the official MCP Python SDK 2.2.0, in a separate virtual environment. The server supports public and confidential clients, S256 PKCE, client/resource-bound rotating refresh, revocation, discovery, dynamic registration and HTTPS Client ID Metadata Documents (CIMD). ```sh python3 -m venv /tmp/citedoor-oauth-example /tmp/citedoor-oauth-example/bin/pip install -r examples/oauth/requirements.txt /tmp/citedoor-oauth-example/bin/python examples/oauth/client.py --server "$HIVEHALL_PUBLIC_URL/mcp" ``` Use the deployment's `HIVEHALL_PUBLIC_URL`, or supply an explicit `--server` MCP URL. There is no baked-in site domain in the client. The browser asks you to sign in, shows the application and callback address, and asks for approval. Leave URLs empty to connect an inert identity; enter exact public URLs to activate Read for those sources. OAuth scope `mcp` is protocol access, not a blanket permission to read the web. Everything the agent does is subject to existing policy and logging. The SDK puts the current access token into live requests and automatically refreshes it. The example atomically saves the replacement access/refresh pair and its absolute expiry in a private 0600 file outside the repository. Client registration also persists, so running the command again resumes the same agent without another approval while the connection remains valid. The lock prevents two example processes rotating one shared refresh token. Use a separate `--store` for a separate agent. macOS/Linux are supported. The loopback callback defaults to port 8858; change `--callback-port` and use a fresh store if the registered address changes. Access lasts at most the configured access TTL (normally one hour). The connection/refresh family expires seven days after consent. Renewal never extends that deadline or any page permission. After expiry or revocation, a new owner approval is required. Refresh tokens rotate. The immediately preceding generation can retry for at most two seconds and receives the same replacement pair, encrypted with the tenant key while retained on the server. This accommodates native clients renewing concurrent stream/request transports; it also delays detection of stolen-token reuse by that brief interval. It never extends a permission or creates an extra token generation. After that window, or once the replacement itself rotates, reuse revokes the whole connection. Set `HIVEHALL_OAUTH_REFRESH_RETRY_SECONDS=0` for strict single-use behaviour; configured values are capped at two seconds. Owner revocation always wins. Revoke through `/oauth/connections` or RFC 7009 `POST /oauth/revoke`. Project keys and device pairing remain separate and supported. For other native OAuth-capable MCP clients, enter only the public `/mcp` URL, without a static Authorization header, and choose that client's connect/login flow. Capability varies by client/version. On 2 October 2026, Claude Code CLI 2.1.284 and Cursor CLI 2026.09.28-64d2043 passed native discovery, refresh after expiry and fresh-process reconnection without another consent. Codex app-server 0.150.1 passed login, a live tool call and restart before expiry, but its refresh check failed in both DCR and automatic CIMD: it reused a consumed token about nine seconds after successful rotation. The trace does not establish the internal client cause. Use a revocable project key for continuous work in this tested Codex version. The recorded checks and reproduction commands are public at `/v1/public/oauth-client-checks`. Desktop sign-in screens and cloud clients were not tested. Static bearer configurations do not auto-refresh: use a project key or an OAuth-capable transport rather than expecting a token file update to affect an already running client. In production set `HIVEHALL_PUBLIC_URL` to the canonical external HTTPS origin, keep session cookies secure, and deploy the additive database migration before API startup: ```sh PYTHONPATH=backend python -m app.db.deployment upgrade ``` Discovery uses the configured canonical issuer, never an incoming Host header. Native code/refresh exchanges require the exact discovered `resource` URI. CIMD client IDs must be public HTTPS URLs with a non-root path, a matching `client_id` in their JSON document, registered redirect URIs and auth method `none`. Fetches are DNS-pinned, limited to 64 KiB, have no redirects and are not cached. The document is revalidated for each new consent. The example accepts `--client-metadata-url https://your-app.example/oauth-client.json` to use CIMD with a published document naming its loopback callback. Confidential clients use dynamic registration and `client_secret_basic` or `client_secret_post`. OIDC identity federation and enterprise identity assertion are separate protocols and are not advertised by this implementation. ## Reproducible integration tests Run from the repository root using the dedicated local test PostgreSQL: ```sh HIVEHALL_OAUTH_SDK_PYTHON=/tmp/citedoor-oauth-example/bin/python \ PYTHONPATH=backend backend/.venv/bin/python -c 'from app.local_runtime import test_backend; test_backend(["backend/tests/test_oauth.py", "backend/tests/test_oauth_sdk.py", "--tb=short"], postgres_fixtures=True, fail_fast=True)' ``` The two SDK tests cover dynamic registration and Client ID Metadata Documents. They also check the example's credential expiry, private file permissions, exclusive lock and real loopback callback with state validation. Each test serves the app on a random loopback port with a private test schema. It discovers metadata, registers, gets consent, establishes MCP, lets the short test access token expire, renews it on live requests, preserves the MCP session, then loads the persisted store in a fresh SDK client and connects without another consent. No production token or workspace is used. The opt-in native-client check runs the installed Codex app-server (DCR and automatic CIMD), Claude Code CLI and Cursor CLI against an isolated PostgreSQL server. It exercises their own OAuth and credential persistence, never an SDK proxy or pre-generated bearer token. Run from `backend`: ```sh .venv/bin/python -m e2e.native_oauth ``` It prints only allowlisted checks and protocol status counts. No model call is needed for authentication checks; account restrictions can still block clients. Desktop UI, ChatGPT cloud and clients not installed locally require separate testing. Success with a static bearer header does not establish OAuth support.