Skip to content

Stable OAuth Auto Verification With Custom IdP For Python #17

Description

@bettercallsaulj

Stable OAuth Auto Verification With Custom IdP For Python

Goal

Provide stable automated OAuth verification for gopher-mcp-python without
using Gmail or another external identity provider as the main CI signal.

The required outcome is to auto-verify OAuth through both endpoint shapes:

  • Direct MCP server endpoint.
  • MCP gateway endpoint.

The custom IdP should verify the Python SDK OAuth plumbing:

  • Direct MCP server and MCP gateway endpoints advertise OAuth protection.
  • SDK discovers protected-resource metadata.
  • SDK discovers authorization server metadata.
  • SDK obtains or refreshes an access token.
  • SDK passes the bearer token to native gopher-orch runtime credentials.
  • The stable test proves this at the SDK/runtime-options boundary.

Gmail or another real IdP can still be used as an optional manual or scheduled
smoke test, but it should not be the primary stable regression test.

Verification Boundary

This design is enough to verify the Python SDK's OAuth auto feature in a
real-protocol way, even though the IdP and MCP server are custom.

It proves:

  • 401 WWW-Authenticate challenge handling.
  • Protected resource metadata discovery.
  • Authorization server metadata discovery.
  • Refresh-token or token exchange behavior.
  • Bearer token propagation into SDK runtime credentials.
  • Protected direct MCP server path after token injection.
  • Protected MCP gateway path after token injection.
  • Optional real native GopherAgent path against a protected MCP test tool.

It does not prove:

  • Gmail-specific refresh-token lifetime or rotation behavior.
  • Google MFA, phone approval, consent, or risk-policy behavior.
  • Workspace admin policy behavior.
  • Provider-specific quirks around scopes, audience, PKCE, or redirect URI
    matching.
  • Hosted Gopher production service availability.

The custom IdP and custom protected MCP endpoint harness should be treated as
the authoritative stable regression test for Python SDK OAuth correctness.
Gmail or another real IdP should be treated as an external compatibility smoke
test.

Whole Verification Picture

Stable custom-IdP CI flow:

┌────────────────────┐
│ GitHub Actions job │
└─────────┬──────────┘
          │ starts
          v
┌────────────────────┐        ┌────────────────────────────┐
│ Custom test IdP    │        │ Custom protected endpoints │
│ - metadata         │        │ - direct MCP server URL    │
│ - token endpoint   │        │ - MCP gateway URL          │
└─────────┬──────────┘        └─────────────┬──────────────┘
          │                                 │
          │                                 │ 401 + WWW-Authenticate
          │                                 │ resource_metadata="..."
          │                                 v
          │                      ┌────────────────────────┐
          │                      │ gopher-mcp-python SDK  │
          │                      └───────────┬────────────┘
          │                                  │ fetch protected resource metadata
          │<─────────────────────────────────┤
          │                                  │ fetch auth server metadata
          │<─────────────────────────────────┤
          │                                  │ refresh-token exchange
          │<─────────────────────────────────┤
          │                                  │ returns test access token
          │─────────────────────────────────>│
          │                                  │ pass bearer token to runtime options
          │                                  v
          │                      ┌────────────────────┐
          │                      │ Native boundary    │
          │                      │ mock or real FFI   │
          │                      └─────────┬──────────┘
          │                                │
          │                                │ Authorization: Bearer test token
          │                                v
          │                    ┌────────────────────────────┐
          │                    │ Direct MCP server endpoint │
          │                    │ and MCP gateway endpoint   │
          │                    └─────────────┬──────────────┘
          │                                  │
          │                                  v
          │                    ┌────────────────────────────┐
          │                    │ OAuth auto verification    │
          │                    │ passes for both endpoints  │
          │                    └────────────────────────────┘

This flow is deterministic and should be the blocking regression signal. It
uses custom/local endpoint implementations, but those implementations expose the
same OAuth and MCP contracts the SDK relies on with real services.

Optional real Gopher/Gmail smoke flow:

┌────────────────────────────┐
│ Manual / scheduled workflow│
└─────────────┬──────────────┘
              │ uses real endpoint URLs
              v
┌────────────────────────────┐        ┌────────────────────────────┐
│ Real Gopher endpoints      │        │ Google OAuth               │
│ - direct MCP server URL    │        │ real IdP / token authority │
│ - MCP gateway URL          │        └─────────────┬──────────────┘
└─────────────┬──────────────┘                      │
              │ 401 + real OAuth metadata           │
              v                                     │
┌────────────────────────────┐                      │
│ gopher-mcp-python SDK      │                      │
└─────────────┬──────────────┘                      │
              │ refreshes or receives Google token  │
              ├─────────────────────────────────────>
              │ receives real access token
              <─────────────────────────────────────┤
              │ pass bearer token to native runtime
              v
┌────────────────────────────┐
│ Native gopher-orch runtime │
└─────────────┬──────────────┘
              │ Authorization: Bearer Google token
              v
┌────────────────────────────┐
│ Real Gopher endpoint       │
│ returns Gmail result       │
└─────────────┬──────────────┘
              v
┌────────────────────────────┐
│ Smoke verification passes  │
│ for server and gateway     │
└────────────────────────────┘

This flow proves the external production integration is currently healthy, but
failures can come from Google, Workspace policy, hosted Gopher availability, or
LLM provider availability. It should complement, not replace, the stable custom
IdP CI flow.

Why Not Gmail As The Main CI Gate

Gmail OAuth is valuable for proving an end-to-end production integration, but it
is not deterministic enough for stable SDK verification.

Risk factors:

  • Refresh tokens can expire, rotate, or be revoked.
  • Google can require MFA, phone approval, consent re-confirmation, or risk-based
    checks.
  • Workspace security policy can change outside the SDK repository.
  • Scopes, app verification, test-user allowlists, and OAuth consent state can
    affect results.
  • Failures often indicate account/provider policy instead of an SDK regression.

The SDK still needs real-provider smoke coverage, but that should be manual,
scheduled, or non-blocking.

Custom IdP Design

Implement a small deterministic OAuth/OIDC-compatible IdP for tests.

The IdP should expose:

GET  /.well-known/openid-configuration
GET  /.well-known/oauth-authorization-server
GET  /.well-known/oauth-protected-resource
POST /token
GET  /authorize
GET  /jwks

For refresh-token-only automated verification, /token is the most important
endpoint. Browser login is not required for the CI path.

Token Endpoint

The token endpoint accepts a fixed test refresh token:

grant_type=refresh_token
client_id=<test-client-id>
client_secret=<test-client-secret>
refresh_token=<test-refresh-token>

Successful response:

{
  "access_token": "test-access-token",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "openid profile email"
}

Failure cases should be deterministic:

  • Wrong refresh token returns invalid_grant.
  • Wrong client credentials return invalid_client.
  • Unsupported grant type returns unsupported_grant_type.
  • Missing fields return clear invalid_request responses.

Tests should assert error messages without printing client secrets, refresh
tokens, access tokens, or provider keys.

Protected MCP Endpoint Behavior

The test MCP endpoint should reject unauthenticated requests with:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="http://127.0.0.1:<port>/.well-known/oauth-protected-resource"

The protected resource metadata should identify the authorization server:

{
  "resource": "http://127.0.0.1:<mcp-port>/mcp",
  "authorization_servers": [
    "http://127.0.0.1:<idp-port>"
  ]
}

The authorization server metadata should include:

{
  "issuer": "http://127.0.0.1:<idp-port>",
  "authorization_endpoint": "http://127.0.0.1:<idp-port>/authorize",
  "token_endpoint": "http://127.0.0.1:<idp-port>/token",
  "jwks_uri": "http://127.0.0.1:<idp-port>/jwks",
  "registration_endpoint": "http://127.0.0.1:<idp-port>/register"
}

Test Coverage

Test Levels

There are two useful verification levels.

SDK OAuth plumbing test:

  • Custom IdP is required.
  • Custom MCP tools are not required.
  • The native boundary can be mocked.
  • The test asserts that SDK OAuth discovery obtains a token and passes
    access_token into the runtime options used by agent_create_by_url.
  • This should be the stable pull-request CI signal because it does not require
    a real LLM provider key or native runtime availability.

End-to-end SDK plus native runtime test:

  • Custom IdP is required.
  • A custom protected MCP server with at least one deterministic tool is
    required.
  • The test creates a real GopherAgent.
  • The native runtime connects to the MCP server with the SDK-provided bearer
    token, discovers tools, invokes the test tool, and returns a result.
  • This is stronger coverage, but it depends on native package availability and
    an LLM provider key, so it is better as an optional or scheduled integration
    job.

Recommended custom MCP tool:

tool: whoami
input: {}
output: { "subject": "test-user@example.test" }

The protected MCP server should behave deterministically:

  • Missing bearer token returns 401 with OAuth metadata challenge.
  • Wrong bearer token returns 401.
  • Expected bearer token allows tool discovery and tool invocation.

Stable CI Tests

Use the custom IdP for blocking CI verification.

Coverage:

  • OAuth discovery from a 401 challenge.
  • Protected resource metadata parsing.
  • Authorization server metadata parsing.
  • Refresh-token exchange.
  • Runtime bearer token injection.
  • Direct MCP server endpoint path.
  • MCP gateway endpoint path.
  • Secret-safe logs and exception messages.
  • Clear failure messages for invalid token, invalid client, unsupported grant,
    and invalid metadata.

GitHub Actions Usage

The custom IdP can run directly inside GitHub Actions.

Recommended workflow shape:

GitHub Actions job
  -> python -m pip install -e ".[dev]"
  -> python -m pytest tests/test_oauth_auto_custom_idp.py
  -> test starts local custom IdP on 127.0.0.1
  -> test starts protected local MCP server endpoint on 127.0.0.1
  -> test starts protected local MCP gateway endpoint on 127.0.0.1
  -> SDK performs OAuth discovery and token flow
  -> SDK passes bearer token into runtime options
  -> test asserts both endpoint shapes work

Use fixed test fixture credentials for this path:

OAUTH_TEST_CLIENT_ID=test-client
OAUTH_TEST_CLIENT_SECRET=test-secret
OAUTH_TEST_REFRESH_TOKEN=test-refresh-token

These values are not real provider secrets, so they do not need to be stored in
GitHub Secrets. They should still be treated as test fixtures and should not be
mixed with Gmail or production IdP credentials.

For stable pull-request CI, prefer testing up to the SDK/runtime-options
boundary with the local custom IdP and mocks for the native boundary. This keeps
the test independent from native package availability, hosted MCP services, and
LLM provider availability.

If a test creates a real GopherAgent, it may still require:

  • native gopher-orch package availability
  • a reachable MCP server implementation
  • an LLM provider API key

That real-native path is useful, but it should be a separate optional or
scheduled integration job. It should not replace the deterministic custom IdP
regression test.

Optional Real IdP Smoke Tests

Keep Gmail verification separate and optional.

Coverage:

  • Real Google token endpoint accepts a manually provided refresh token.
  • Real Gmail-backed MCP server returns the expected profile.
  • Real gateway path works against the hosted Gopher environment.

These tests should run manually or on a non-blocking schedule because failures
may be caused by Google account policy rather than SDK behavior.

Custom IdP vs Real IdP

Area Custom IdP Real IdP such as Gmail
Stability Deterministic and controlled by test code Can fail due to provider policy or account state
MFA / phone checks Not included in CI path May be required unexpectedly
Refresh token lifetime Controlled by test fixture Provider-specific; can expire or be revoked
Error behavior Exact responses can be asserted Spec-compatible but provider-specific
SDK OAuth discovery Fully testable Fully testable
SDK token exchange Fully testable for spec behavior Tests provider-specific behavior too
Runtime bearer injection Fully testable Fully testable
Regression signal Strong SDK signal Mixed SDK/provider signal

Recommended Implementation

Primary implementation should live in gopher-mcp-python.

Why:

  • The feature under verification is Python SDK OAuth auto behavior.
  • The test should live close to Python discovery, token exchange, token store,
    runtime option, and create_with_url behavior.
  • The stable pull-request signal should not depend on native release
    availability or hosted services.
  • The implementation should stay aligned with gopher-mcp-js so both SDKs
    verify the same OAuth contracts.

Recommended locations:

gopher-mcp-python/tests/helpers/
gopher-mcp-python/tests/test_oauth_auto_custom_idp.py
gopher-mcp-python/tests/test_custom_oauth_test_idp.py
gopher-mcp-python/tests/test_custom_protected_mcp_endpoints.py
gopher-mcp-python/tests/test_oauth_test_token_helper.py

Recommended Python helper shape:

tests/helpers/oauth_test_token.py
tests/helpers/custom_oauth_test_idp.py
tests/helpers/custom_protected_mcp_endpoints.py

The stable test should use existing public factory APIs. Do not introduce a new
factory name only for OAuth verification. OAuth support should remain on
GopherAgent.create_with_url and other existing create methods.

Use gopher-orch only for native-specific follow-up coverage, such as proving
the native runtime can consume a Python SDK-provided bearer token and invoke a
protected MCP tool. That can be a lower-level native integration test in
gopher-orch, or an optional native integration job in gopher-mcp-python if
it is practical.

Implementation Order

  1. Add a generic test OAuth token helper that can refresh against any token
    endpoint.
  2. Add a local custom IdP test harness under tests/helpers.
  3. Add a protected endpoint harness that models both direct MCP server and MCP
    gateway URLs.
  4. Add the stable Python SDK OAuth auto test for direct MCP server endpoint
    using GopherAgent.create_with_url.
  5. Add the gateway endpoint case with parallel assertions.
  6. Add deterministic failure-mode tests for invalid grant, invalid client,
    unsupported grant, wrong bearer token, invalid metadata, and secret-safe
    failures.
  7. Add a pytest command or script for the focused suite, for example:
python -m pytest \
  tests/test_oauth_auto_custom_idp.py \
  tests/test_oauth_auto_custom_idp_failures.py \
  tests/test_custom_oauth_test_idp.py \
  tests/test_custom_protected_mcp_endpoints.py \
  tests/test_oauth_test_token_helper.py
  1. Add GitHub Actions integration that runs the focused suite on pull requests
    without any GitHub Secrets.
  2. Document the custom IdP verification path in repository docs.
  3. Add an optional native end-to-end smoke test only if needed.
  4. Keep Gmail live verification manual, scheduled, or non-blocking.

Expected Result

The Python SDK has a stable automated OAuth regression suite that proves the
OAuth feature works without depending on Gmail account state. Gmail remains
useful as a real-provider smoke test, but custom IdP verification becomes the
authoritative CI signal for Python SDK correctness.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions