Skip to content

[Client] Support completing OAuth authorization in a later request - #660

Merged
chr-hertel merged 13 commits into
modelcontextprotocol:mainfrom
bbdu3:fix/web-oauth-authorization
Nov 10, 2026
Merged

chr-hertel merged 13 commits into
modelcontextprotocol:mainfrom
bbdu3:fix/web-oauth-authorization

Conversation

@bbdu3

@bbdu3 bbdu3 commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Support deferred OAuth authorization for web applications, allowing the authorization code flow to resume through a subsequent callback request, including across different PHP workers.

Adds RedirectAuthorizationHandler, AuthorizationPendingException, and OAuthAuthenticator::finishAuthorization(), with persistent, single-use authorization state and regression tests.

Fixes #630.

Motivation and Context

AuthorizationHandlerInterface::authorize() currently expects callback parameters within the same call, which does not fit web applications where authorization redirects and callbacks are handled by separate HTTP requests.

This change implements the approach :

  • The handler throws AuthorizationPendingException with the authorization URL.
  • The authenticator persists the pending flow before propagating the exception.
  • The callback route calls finishAuthorization() to validate the response, exchange the authorization code, and store the tokens.
  • The application can then retry the original MCP operation.

Test

Unit tests

All 54 OAuthAuthenticator tests pass, covering cross-instance completion, PKCE context preservation, state validation and replay prevention, issuer validation, user storage isolation, and offline_access fallback.

Storage tests cover interleaved writes, atomic consumption across instances, issuer-specific cleanup, and malformed records.

HTTP integration tests

All 10 OAuth integration tests pass against local authorization and MCP server fixtures.

The cross-process regression test verifies that:

  • Authorization can be completed in separate PHP processes.
  • Only one competing callback succeeds.
  • The persisted token can access a protected MCP tool.
  • Replayed callbacks are rejected.

Broader checks

  • Full unit suite: 1,742 tests, with the same 5 pre-existing Windows-related failures and 1 skipped test as the baseline. i confirmed these failures are not regression introduced in this PR.
  • PHPStan: same 5 pre-existing errors, no new errors.
  • PHP CS Fixer passes for changed PHP files.
  • git diff --check passes.

Breaking Changes

None. Existing interfaces and synchronous authorization remain compatible.

CredentialStorageInterface is unchanged. Deferred authorization requires:

  • Persistent storage implementing PendingAuthorizationStorageInterface.
  • Catching AuthorizationPendingException and redirecting to its authorization URL.
  • Recreating the authenticator in the callback route with the same configuration and session-scoped storage.
  • Calling finishAuthorization() before retrying the MCP operation.

Types of Changes

  • Bug fix (non-breaking change)
  • New feature (non-breaking change)
  • Documentation update

@bbdu3
bbdu3 changed the base branch from main to feat-client-auth November 4, 2026 09:39
@chr-hertel chr-hertel added this to the 1.0.0 milestone Nov 4, 2026
@chr-hertel chr-hertel added Client Issues & PRs related to the Client component auth Issues and PRs related to Authentication / OAuth labels Nov 4, 2026
@chr-hertel chr-hertel modified the milestones: 1.0.0, 0.10.0 Nov 8, 2026
@chr-hertel
chr-hertel force-pushed the fix/web-oauth-authorization branch from f9f0073 to 4134ed6 Compare November 10, 2026 08:17
@chr-hertel
chr-hertel changed the base branch from feat-client-auth to main November 10, 2026 08:17
@chr-hertel

chr-hertel commented Nov 10, 2026 •

Copy link
Copy Markdown
Member

Thanks @bbdu3 for working on this! The client auth stack moved quite a bit underneath you - #663, #664, #666, #668 and #669 are on main now. So I ported your commit onto main, fixed what broke on the way and retargeted the PR - your commit stays first, my fixes are on top.


  • adapted to main: OAuthErrorException instead of matching messages, tokens keyed by audience, state checked before the error
  • Client::connect() wrapped the exception in a ConnectionException, so the documented catch (AuthorizationPendingException) never fired - it's rethrown as it is now, and HttpTransport doesn't log it as an error anymore
  • the pending authorization kept only part of the metadata - without code_challenge_methods_supported a restarted flow gets refused
  • it also stored the whole client, incl. a configured client_secret - a configured client is now resolved again on the callback, only a dynamic registration is kept, so another flow re-registering doesn't break the callback
  • finishAuthorization() checks the expected issuer and forgets a registration the server answered invalid_client for, like the normal flow
  • audience always equals the resource on main, so I dropped it
  • OAuth::build() refuses RedirectAuthorizationHandler without setCredentialStorage(), since the default in-memory storage loses the pending state with the request
  • PHPStan errors in the tests and the fixture, TestDox on the new tests, docs rewritten and the changelog entry moved to 0.10.0

One more thing: the commit's author email reads gamil.com and the sign-off says unknown - want to fix that?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The documentation must require session initialization on both routes before deriving the credential path, otherwise browsers can share a credential file and reuse another user’s access token.

1 open issue
What changed in this PR

Adds deferred OAuth authorization to the PHP client's authentication flow, allowing web applications to complete authorization in a later callback request, including on another PHP worker.

Changes:

  • Adds redirect-based authorization and finishAuthorization() while preserving synchronous handlers.
  • Persists expiring, single-use authorization state and propagates pending authorization exceptions.
  • Documents the web callback flow and adds unit and cross-process integration coverage.
File Description
tests/​Unit/​Client/​Transport/​HttpTransportTest.php Tests pending authorization propagation without retries or error logging.
tests/​Unit/​Client/​Auth/​OAuthTest.php Tests the redirect handler's storage requirement.
tests/​Unit/​Client/​Auth/​OAuthAuthenticatorTest.php Covers deferred completion, validation, replay prevention, and fallback.
tests/​Unit/​Client/​Auth/​FileCredentialStorageTest.php Tests pending state persistence, consumption, and cleanup.
tests/​Unit/​Client/​Auth/​AuthorizationServerMetadataTest.php Tests metadata serialization round trips.
tests/​Integration/​Fixture/​oauth/​finish-authorization.php Completes authorization in a separate process.
tests/​Integration/​ClientOAuthTest.php Tests cross-process completion and competing callbacks.
src/​Exception/​AuthorizationPendingException.php Adds an exception carrying the authorization URL.
src/​Client/​Transport/​HttpTransport.php Propagates pending authorization without error handling.
src/​Client/​Auth/​RedirectAuthorizationHandler.php Defers authorization through the new exception.
src/​Client/​Auth/​PendingAuthorizationStorageInterface.php Defines pending state storage and atomic consumption.
src/​Client/​Auth/​PendingAuthorization.php Represents and serializes pending authorization context.
src/​Client/​Auth/​OAuthAuthenticator.php Saves pending flows and completes callbacks.
src/​Client/​Auth/​OAuth.php Requires explicit storage for redirect authorization.
src/​Client/​Auth/​InMemoryCredentialStorage.php Adds in-memory pending state management.
src/​Client/​Auth/​FileCredentialStorage.php Persists and atomically consumes pending state.
src/​Client/​Auth/​AuthorizationServerMetadata.php Adds metadata serialization for persisted flows.
src/​Client/​Auth/​AuthorizationHandlerInterface.php Documents the pending authorization exception.
src/​Client.php Preserves pending authorization exceptions during connection.
docs/​client/​authorization.md Documents session-scoped storage and callback handling.
CHANGELOG.md Records deferred OAuth authorization support.

Review effort: Balanced

Comment thread docs/client/authorization.md Resolved

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The server-side OAuth example omits setApplicationType('web'), leaving the native default and risking rejection of its HTTPS callback by authorization servers enforcing native-app redirect restrictions.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Low severity Set the OAuth application type to web

docs/​client/​authorization.md:105

Please add setApplicationType('web') to this server-side example. OAuth defaults to native, and neither the redirect URI nor the handler changes that setting, so the example registers as a native application; authorization servers enforcing native-app redirect restrictions can reject this HTTPS callback.

1 resolved since last review

Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Human review is needed to assess an unpublished concern about test cleanup leaving hidden lock files in the system temporary directory.

No open issues 🎉

Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Human assessment is needed because issuer-wide client re-registration can invalidate another pending OAuth flow, causing a valid callback to fail after its state has been consumed.

No open issues 🎉

Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Human assessment is needed because a critical concurrency race can allow a rejected stale callback to delete another worker’s newer registration, tokens, and pending authorizations.

No open issues 🎉

Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The deferred OAuth authorization change has no active review issues.

No open issues 🎉

Review effort: Balanced

@chr-hertel chr-hertel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @bbdu3!

@chr-hertel
chr-hertel merged commit da5d0d7 into modelcontextprotocol:main Nov 10, 2026
30 checks passed
@bbdu3
bbdu3 deleted the fix/web-oauth-authorization branch November 11, 2026 01:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auth Issues and PRs related to Authentication / OAuth Client Issues & PRs related to the Client component

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Client] Let web applications finish the authorization code flow in a later request

3 participants