You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Client] Support completing OAuth authorization in a later request - #660
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.
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:
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?
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.
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.
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
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.
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.
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.
…l flow
Resolve the client again instead of storing it, so a configured secret stays
out of the storage. Check the expected issuer, forget a registration the server
refused, and drop the audience field, which always equals the resource.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
authIssues and PRs related to Authentication / OAuthClientIssues & PRs related to the Client component
3 participants
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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, andOAuthAuthenticator::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 :
AuthorizationPendingExceptionwith the authorization URL.finishAuthorization()to validate the response, exchange the authorization code, and store the tokens.Test
Unit tests
All 54
OAuthAuthenticatortests pass, covering cross-instance completion, PKCE context preservation, state validation and replay prevention, issuer validation, user storage isolation, andoffline_accessfallback.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:
Broader checks
git diff --checkpasses.Breaking Changes
None. Existing interfaces and synchronous authorization remain compatible.
CredentialStorageInterfaceis unchanged. Deferred authorization requires:PendingAuthorizationStorageInterface.AuthorizationPendingExceptionand redirecting to its authorization URL.finishAuthorization()before retrying the MCP operation.Types of Changes