Repository navigation
feat(passport): add oauth consent screen helpers for v1.5.0 - #24
Merged
Merged
Conversation
…n can't show A self-registered OAuth client names itself, so the destination of its redirect URI is the part a person can check before approving it. OAuthRedirectTarget gives a consent screen that destination: the scheme, host and port, the punycode form of an internationalized domain that could imitate another site's name, and whether it is an application on the person's own computer. It uses parse_url and the IDN functions, so it works on PHP 8.3. OAuthRedirectUri now refuses a redirect URI whose host isn't a valid domain name or IP address: a right-to-left override, invalid punycode, or a percent-encoded / or @ would make the host read as a different site.
…nsent screen A self-registered client, such as an MCP client, keeps its client ID after the server deletes or revokes it, and keeps sending people to the authorization screen with it. Passport answers with its 401 invalid_client JSON, which the person sees in their browser. DetectUnknownOAuthClient, added to Passport's routes, throws UnknownOAuthClientException (400) in that case for the application to render as a page. It acts only on the authorization screen and only when the client is missing or revoked: the token endpoint still answers clients with JSON, and Passport's answer to an unregistered redirect URI for a client that exists is unchanged. Passport's OAuthServerException is an HttpResponseException, which the router returns before the exception handler sees it, so this has to be middleware rather than an exception mapping.
…rent hosts OAuthRedirectTarget read hosts with parse_url(), which differs from a browser's URL parser. For https://evil.example\@app.example.edu it showed app.example.edu, while a browser reads the backslash as a slash and goes to evil.example. It now refuses, rather than guesses at, any URI the two could read differently: whitespace or control characters, a backslash, credentials, a port that isn't a number, and a host a browser rewrites as an IPv4 address (0x7f.1 is 127.0.0.1). IPv6 hosts are shown in their shortest form, and the IDN conversion checks the bidi and joiner rules. Compared with PHP 8.5's WHATWG parser over 125,000 generated URIs, every URI it accepts shows the host a browser would go to. OAuthRedirectUri uses it, so the rule refuses the same URIs at registration.
📊 Code Coverage Report
Coverage by File
|
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Overview
DetectUnknownOAuthClientis a middleware for Passport's routes. It throwsUnknownOAuthClientException(400) when a person reaches the authorization screen from a client that's been deleted or revoked, so the application can render a page. Today Passport answers that request with its 401invalid_clientJSON, and the person sees it in their browser. This happens most with self-registered clients such as MCP clients: one keeps its client ID after the server prunes or revokes it, and keeps sending people back with it.passport.authorizations.authorize, and only when the client is missing or revoked. The token endpoint still answers clients with JSON, which some of them act on by registering again. Passport also answersinvalid_clientfor a redirect URI the client didn't register; that response is unchanged, because the client still exists.OAuthServerExceptionis anHttpResponseException, which the router returns before the exception handler sees it, so$exceptions->map()never runs.OAuthRedirectTargetgives a consent screen the destination to show for a redirect URI. A client's name is only a claim; where it sends the person is the part they can check. It provides the scheme, host and port (https://bücher.example), the punycode form of an internationalized domain that could imitate another site's name (https://xn--bcher-kva.example), and whether the destination is on the person's own computer. It usesparse_urland the IDN functions, so it works on PHP 8.3.OAuthRedirectTargetandOAuthRedirectUrirefuse what they can't show truthfully. A consent screen must show the host the browser will actually go to, but PHP'sparse_url()and a browser's URL parser disagree on some URIs:https://evil.example\@app.example.eduisapp.example.eduto PHP andevil.exampleto a browser. Both now refuse those URIs instead of guessing: whitespace or control characters, a backslash, credentials, a port that isn't a number, a host a browser rewrites as IPv4 (0x7f.1), a right-to-left override, invalid punycode, or a host that decodes to/or@. Compared with PHP 8.5's WHATWG parser over 125,000 generated URIs, every URI it accepts shows the host a browser would go to. The rule change is under "Changed" in the CHANGELOG.Checklist
composer checkpasses: the repository has nocheckscript; Rector (no changes), Pint, PHPStan and the Unit and Feature suites (460 tests) passv1.5.0, dated 2026-10-07, with compare links