Split the registration request model from the registered-client record - #3181
Split the registration request model from the registered-client record#3181maxisbey wants to merge 4 commits into
Conversation
📚 Documentation preview
|
There was a problem hiding this comment.
All reported issues were addressed across 11 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
There was a problem hiding this comment.
Beyond the inline nits, two candidate concerns were examined and ruled out: check_registration_usable accepting private_key_jwt (correct — PrivateKeyJWTOAuthProvider supplies the assertion, and the recognized set is derived from the same TokenEndpointAuthMethod literal the SDK sends, so the refresh path a private-key-JWT client inherits keeps working), and an empty-string client_id bypassing the new client_id-REQUIRED invariant on the record (not a practical issue — "" still parses but downstream guards treat a falsy client_id as absent, and no observed server mints an empty id).
Extended reasoning...
Bugs were found (three nits, posted inline), so the review body is limited to recording what else was examined this run. The two refuted candidates both target the new tolerance/usability mechanism in src/mcp/client/auth/oauth2.py and src/mcp/shared/auth.py; verifiers concluded neither is a real defect. This note is informational only — it is not an approval, and the PR (a breaking change across OAuth client/server auth code) should still get human review.
There was a problem hiding this comment.
All reported issues were addressed across 10 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
|
Follow-up in 29a3942 addressing the review round; the six inline threads are replied to and resolved individually. Two further items came in without a thread, so recording them here: Reported: required Reported: the SDK-internal Also folded in from the inline threads: |
There was a problem hiding this comment.
Beyond this run's inline notes, two other candidates were examined and ruled out: (1) widening the record's token_endpoint_auth_method to str does not break client_auth.py — its unsupported-method branch raises a handled AuthenticationError (client_auth.py:94-97), not an asserted-unreachable, and SDK-registered records are still built from the strict request model; (2) the record's now-required client_id accepting "" is not a regression — pre-PR the field was optional and an absent client_id parsed as success, so requiring it strictly narrows what passes.
Extended reasoning...
Bugs were found and posted inline, so no approval or full deferral. This note records two candidate issues that were examined and refuted this run, so later review passes don't re-explore them from scratch: the widened auth-method type on the record versus the bundled AS middleware's unsupported-method branch (verified to be an explicit, handled error path), and the empty-string client_id parse (verified to be a strict improvement over the previous optional field). Informational only — not a guarantee of correctness.
There was a problem hiding this comment.
No new issues found in this pass over 884bf91 — all items from the previous review rounds are addressed — but this is a breaking change to the public auth models and a rewrite of the parse boundary for untrusted registration responses, so it warrants a maintainer's review rather than an auto-approval.
What was reviewed this pass:
- the pre-validation
issuerdrop inhandle_registration_response— theexcept ValueErrorcovers bothfrom_jsonandValidationErrorlayers without over-catching - the widened placeholder stripper against non-string members —
client_secret_expires_at: 0and other falsy-but-meaningful values survive (0 != "") - removal of the
client_idtruthiness guards inprepare_token_auth— safe now that the record requiresclient_idand the placeholder rule rejects an empty one - the new secret-not-issued gate — a
""client_secret reads as absent via the placeholder rule, so it correctly trips the gate before the browser round-trip
Extended reasoning...
Overview
PR #3181 splits OAuthClientInformationFull (the registered-client record parsed from an untrusted DCR response) from OAuthClientMetadata (the strict registration request), making them siblings over OAuthClientMetadataBase. It touches src/mcp/shared/auth.py, the client auth flow (oauth2.py, utils.py), the bundled server registration handler, docs (migration.md, oauth-clients.md), and adds substantial model/interaction test coverage. Head commit 884bf91 is the third revision, addressing two prior review rounds from me and one finding from cubic.
Security risks
This is squarely security-sensitive territory: it changes how the client parses an untrusted authorization-server response, how the SEP-2352 issuer trust binding is populated (now sealed — dropped from the wire body before validation rather than parsed-then-cleared), which token-endpoint auth methods the flow will act on, and the bundled AS's registration echo. The direction of each change is defensive — parse-tolerant on cosmetic metadata, but client_id is now required, an empty client_id is rejected, unusable credential assignments fail closed before persistence or any browser round-trip, and the server refuses private_key_jwt registrations it could never authenticate. I found no injection, bypass, or exposure vector in this pass; the earlier issuer-seeding concern is resolved at the correct layer.
Level of scrutiny
High — production client-auth code plus an intentional breaking API change (documented in docs/migration.md per repo policy). The design decisions here (sibling split, parse-tolerant record, registration-time usability gate, required client_id, the deliberately-deferred server-record/client-response separation named in the PR description) are exactly the kind of API-shape calls the guidelines say a human maintainer should own, regardless of correctness. That, not any outstanding defect, is why this defers rather than approves.
Other factors
All prior review threads are resolved with code changes rather than pushback (one item — defaulting an omitted auth method to client_secret_basic — was reasonably declined as an out-of-scope semantic change). Test coverage is strong: model-level parametrization over placeholder styles, real handle_registration_response paths including malformed/non-object/non-UTF-8 bodies, end-to-end interaction tests asserting no authorize/token traffic and nothing persisted on the unusable paths, and server-side 201-body assertions where the earlier omission actually lived. The repo requires 100% coverage in CI, which backstops the branch-level claims.
|
Second review round addressed in 884bf91; the four new inline threads are replied to and resolved individually. Three further reported items had no thread, so recording them here: Reported (again): required Reported: stale prose about auth-method tolerance. Both bits valid and fixed. The comment, test docstring, and migration line no longer claim the Reported: bundled AS confirms Two more from the inline threads worth naming in one place: an SDK-internal One item remains deliberately out of scope — treating an omitted |
OAuthClientInformationFull, the client's parse of the authorization server's Dynamic Client Registration response, inherited from OAuthClientMetadata, the request the client sends. That typed the response as though it had to be a request this SDK would send. RFC 7591 3.2.1 says otherwise: the server may reject or replace any requested metadata value, and real servers echo an application_type outside OIDC Registration's web/native, an explicit null, an auth method the SDK does not implement, or an empty redirect_uris. Each raised ValidationError on a 2xx response, after the server had already provisioned the client, so the registration was discarded and orphaned. Make the two models siblings over a shared OAuthClientMetadataBase. The request keeps its strict types, so the SDK still refuses to send an unregistered application_type. The record accepts what a server may echo: application_type and token_endpoint_auth_method are str | None (with an echoed "" read as absent, as the optional URL fields already were), grant_types is list[str], and redirect_uris may be absent or empty. client_id is now required, as RFC 7591 3.2.1 makes it in the response. Whether a substituted value is usable is judged where it matters, not at parse: an auth method the client cannot apply is reported as an OAuthRegistrationError when the registration completes, before the record is stored or any interactive authorization begins, and prepare_token_auth reports the same for a stored record. The recognized set is derived from the one TokenEndpointAuthMethod type so the two cannot drift. The bundled registration endpoint now returns all registered metadata in its 201 response, building the record from the validated request's dump so a field can no longer be silently dropped from the echo; it previously omitted application_type, reporting the default in place of a client's "web".
…boundary Two conflated questions shared one recognized-method set: whether the authorization-code registration flow can act on an assigned method, and whether a stored record may carry a method without the token step erroring. The flow authenticates with the minted secret and holds no key to sign a private_key_jwt assertion, so it now rejects that method at registration too, while prepare_token_auth still tolerates it for PrivateKeyJWTOAuthProvider's inherited refresh path. An explicit JSON null in the registration response is now read as an omitted key across the whole record, so grant_types and response_types (which pydantic rejects null for) parse like the other fields. The SDK-internal issuer binding is cleared after parsing, so a body echoing an "issuer" member cannot seed it. The bundled registration endpoint sets client_secret_expires_at to 0 rather than omitting it whenever a client_secret is issued with no expiry configured, as RFC 7591 3.2.1 requires. The migration entry now names both exception surfaces.
An SDK-internal field must never be sourced from the wire, not sourced then cleared: the registration response body is now parsed with issuer dropped before validation, so an echoed member neither seeds the SEP-2352 binding nor fails the parse when malformed. The parse goes through pydantic's JSON layer so malformed bytes still surface as OAuthRegistrationError. Both placeholder styles servers use for unset members - null, and "" - now read as omitted keys across the whole record, closing the gap for list fields and retiring the field-by-field empty-string coercions. An empty client_id is accordingly no client_id, so the unrepresentable empty-identifier record and its dead guards in prepare_token_auth go. check_registration_usable now also rejects a secret-based method for which the server issued no client_secret, before persistence and before any interactive authorization, instead of letting the token request go out unauthenticated after the browser round-trip. The bundled registration endpoint refuses private_key_jwt with 400 invalid_client_metadata: it authenticates token requests with the secret it mints and holds no client key to verify an assertion, so confirming the method would register a client it could never authenticate - which this SDK's own client would then immediately declare unusable, orphaning the record. Correct prose that overstated the private_key_jwt refresh path (the tolerance keeps prepare_token_auth from raising so a rejected refresh can fall back to a fresh, signed client-credentials exchange), the stale Raises text in prepare_token_auth, and the client failure-model page, which described OAuthRegistrationError as only a refusal.
884bf91 to
40fe046
Compare
There was a problem hiding this comment.
No new findings this pass — the latest revision addresses everything raised in the two earlier inline-review rounds — but this is a breaking change to the OAuth model hierarchy and client auth flow, so it should get a human sign-off rather than a bot approval.
What was reviewed this pass:
- The placeholder-as-omitted model validator now covers both
nulland""uniformly, including list-typed fields, and an emptyclient_idcorrectly fails as a missingclient_id. check_registration_usablenow rejects all three unusable-registration shapes (unimplemented method,private_key_jwt, secret-based method with no secret issued) before the record is persisted, matching its docstring.issueris dropped from the untrusted body before validation, so an echoed value can neither seed the SEP-2352 binding nor fail the parse; the bundled AS'smodel_dump()-based 201 echo round-trips through the record without losing fields (pinned by the field-superset test).
Extended reasoning...
Overview
This PR splits OAuthClientInformationFull (the client's parse of a Dynamic Client Registration response, and the SDK's own AS's stored-client type) from OAuthClientMetadata (the registration request) into siblings over a shared base in src/mcp/shared/auth.py. It adds a registration-usability gate (check_registration_usable) and an unknown-method check in prepare_token_auth in src/mcp/client/auth/oauth2.py, hardens handle_registration_response (issuer stripped pre-validation, ValueError chain), and rebuilds the bundled registration endpoint's 201 echo from the validated request's dump, refusing private_key_jwt registrations. Docs (docs/migration.md, docs/client/oauth-clients.md) and an extensive test surface (model-level, handler-level, end-to-end interaction) are updated in the same PR.
Security risks
The changed code is squarely security-sensitive: it defines how an untrusted authorization-server response is parsed, which server-assigned credentials the client will act on, and what the SDK's own AS echoes at registration. The main risks were examined across the two review rounds and this pass: the SEP-2352 issuer trust binding is now never sourced from the wire (dropped before validation, with tests for string and non-string echoes); the tolerant record cannot parse a success without a client_id (required per RFC 7591 §3.2.1, and an empty-string client_id reads as missing); the request model stays strict, so nothing this SDK sends is widened; and unusable server-assigned auth methods fail closed with OAuthRegistrationError before persistence or any authorize/token traffic (asserted end-to-end). The relaxations are confined to parse-side tolerance of fields the flow either does not use (application_type) or judges explicitly at use sites.
Level of scrutiny
This warrants the highest scrutiny tier: it is a deliberate breaking change to public auth types on the v2 branch, it alters the parse boundary for untrusted input, and the record type still serves double duty as the server-side stored-client type — a design seam the author explicitly names as deliberately unfinished. Those are exactly the kinds of API-design and trust-boundary decisions a maintainer should weigh in on, independent of correctness. Bot approval is inappropriate regardless of how clean this pass came back.
Other factors
Two earlier review rounds (mine and cubic's) surfaced eight distinct issues; every one was either fixed with a targeted commit and a pinned test (null/"" placeholder generalization, secret-not-issued gate, issuer-before-validation, RFC 7591 client_secret_expires_at: 0, private_key_jwt refusals on both client and server side, doc corrections) or explicitly declined with a defensible scope argument (defaulting an omitted auth method to client_secret_basic — pre-existing behavior, deferred to its own issue). All inline threads are resolved, this run's bug hunting found nothing new, and I re-verified the four claims in the message against the current code. Coverage is 100% per repo policy and the interaction tests exercise the new failure modes over real HTTP. The remaining call is design judgment, not bug risk — hence defer rather than approve.
OAuthTokenError is no longer only the token endpoint's rejection: a stored client record whose token_endpoint_auth_method this client cannot apply raises it while the token request is being built, before the endpoint is contacted. Say so, parallel to the OAuthRegistrationError half of the same sentence. No-Verification-Needed: doc-only change
|
Third pass. Two items: Reported (fourth time): required Reported: the |
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="docs/client/oauth-clients.md">
<violation number="1" location="docs/client/oauth-clients.md:134">
P2: Stored `private_key_jwt` records and secret-based records missing `client_secret` are not rejected while building the token request; they fall through and yield an unauthenticated request. Narrow this to unrecognized methods, or document the other cases separately.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| ## When it fails | ||
|
|
||
| When the OAuth flow goes wrong, the provider raises an `OAuthFlowError` from `mcp.client.auth`. It has two subclasses. `OAuthRegistrationError` means the authorization server refused to register you. `OAuthTokenError` means the token endpoint said no. One `except OAuthFlowError:` covers discovery, registration, authorization, and exchange. | ||
| When the OAuth flow goes wrong, the provider raises an `OAuthFlowError` from `mcp.client.auth`. It has two subclasses. `OAuthRegistrationError` means registration did not yield a client you can use: the authorization server refused to register you, or it did register you but with credentials this flow cannot use (for instance an authentication method it does not implement). `OAuthTokenError` means a token could not be obtained: the token endpoint said no, or a stored client record carries an authentication method this client cannot apply, which is reported while building the token request rather than sent. One `except OAuthFlowError:` covers discovery, registration, authorization, and exchange. |
There was a problem hiding this comment.
P2: Stored private_key_jwt records and secret-based records missing client_secret are not rejected while building the token request; they fall through and yield an unauthenticated request. Narrow this to unrecognized methods, or document the other cases separately.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/client/oauth-clients.md, line 134:
<comment>Stored `private_key_jwt` records and secret-based records missing `client_secret` are not rejected while building the token request; they fall through and yield an unauthenticated request. Narrow this to unrecognized methods, or document the other cases separately.</comment>
<file context>
@@ -131,7 +131,7 @@ There is one more no-human situation: the client belongs to an enterprise whose
## When it fails
-When the OAuth flow goes wrong, the provider raises an `OAuthFlowError` from `mcp.client.auth`. It has two subclasses. `OAuthRegistrationError` means registration did not yield a client you can use: the authorization server refused to register you, or it did register you but with credentials this flow cannot use (for instance an authentication method it does not implement). `OAuthTokenError` means the token endpoint said no. One `except OAuthFlowError:` covers discovery, registration, authorization, and exchange.
+When the OAuth flow goes wrong, the provider raises an `OAuthFlowError` from `mcp.client.auth`. It has two subclasses. `OAuthRegistrationError` means registration did not yield a client you can use: the authorization server refused to register you, or it did register you but with credentials this flow cannot use (for instance an authentication method it does not implement). `OAuthTokenError` means a token could not be obtained: the token endpoint said no, or a stored client record carries an authentication method this client cannot apply, which is reported while building the token request rather than sent. One `except OAuthFlowError:` covers discovery, registration, authorization, and exchange.
Not everything is a flow error. The network can still fail; those are ordinary `httpx2` exceptions and pass through untouched.
</file context>
| When the OAuth flow goes wrong, the provider raises an `OAuthFlowError` from `mcp.client.auth`. It has two subclasses. `OAuthRegistrationError` means registration did not yield a client you can use: the authorization server refused to register you, or it did register you but with credentials this flow cannot use (for instance an authentication method it does not implement). `OAuthTokenError` means a token could not be obtained: the token endpoint said no, or a stored client record carries an authentication method this client cannot apply, which is reported while building the token request rather than sent. One `except OAuthFlowError:` covers discovery, registration, authorization, and exchange. | |
| When the OAuth flow goes wrong, the provider raises an `OAuthFlowError` from `mcp.client.auth`. It has two subclasses. `OAuthRegistrationError` means registration did not yield a client you can use: the authorization server refused to register you, or it did register you but with credentials this flow cannot use (for instance an authentication method it does not implement). `OAuthTokenError` means a token could not be obtained: the token endpoint said no, or a stored client record carries an unrecognized authentication method, which is reported while building the token request rather than sent. One `except OAuthFlowError:` covers discovery, registration, authorization, and exchange. |
There was a problem hiding this comment.
No new issues found in this pass over the revised code — the follow-up commits resolve all previously raised inline comments as described — but this is a large, breaking change to OAuth client/server auth models, so it still needs a human maintainer's sign-off.
What was reviewed this pass:
- The reworked
check_registration_usable(all three unusable cases, including a secret-based method with no issued secret) and its interaction tests. - The generalized placeholder rule in
_placeholder_members_read_as_omitted— null and "" both read as omitted for scalar and list fields, and an emptyclient_idis now an ordinary missing-client_idrejection. - The pre-validation
issuerstrip inhandle_registration_response(never sourced from the wire; a malformed value cannot fail the parse) and the updated failure-model prose on the client OAuth page. - Checked that every
OAuthClientInformationFullconstruction site insrc/(CIMD helper, client-credentials, private-key-JWT, identity-assertion providers) supplies the now-requiredclient_id— all do.
Extended reasoning...
Overview
This PR splits OAuthClientInformationFull (the registered-client record parsed from a DCR response) from OAuthClientMetadata (the registration request) into siblings over a shared base, making the response parse tolerant of RFC 7591 §3.2.1 server substitutions while keeping the request strict. It also adds a registration-usability gate (check_registration_usable), seals the SDK-internal issuer binding against the untrusted response body, fixes the bundled AS's 201 echo (full metadata, client_secret_expires_at, private_key_jwt refusal), and documents the breaking changes in docs/migration.md. Twelve files across client auth, server auth handlers, shared models, docs, and tests.
Security risks
The changes are squarely in security-sensitive territory: OAuth client registration parsing, token-endpoint client authentication, and the SEP-2352 issuer trust binding. The direction of the changes is defensive — the issuer field is now dropped from the wire body before validation rather than parsed-then-cleared, unusable registrations fail before persistence or any browser round-trip, and the request side stays strict. Prior review rounds probed the loosened parse (placeholder handling, secret-not-issued, issuer seeding) and each gap found was closed with tests. Nothing new surfaced this pass. The residual risk is inherent to widening a parse boundary on untrusted input, which is exactly why a human should weigh the design.
Level of scrutiny
High. This is a breaking API change (isinstance relationships, required client_id, moved validators) in production auth code used by both the client flow and the SDK's bundled authorization server. Per the approval guidelines, security-sensitive auth code should not be shadow-approved regardless of how clean a single review pass is.
Other factors
Three review rounds (two from this reviewer, one from cubic) produced ten distinct findings; the author addressed or explicitly scoped out each one, with the one deferral (defaulting an omitted token_endpoint_auth_method to client_secret_basic) reasonably argued as pre-existing behavior deserving its own issue. Test coverage is strong: model-level parametrized tests, real handle_registration_response paths, end-to-end interaction tests asserting no authorize/token traffic and nothing persisted on unusable registrations, and a structural test pinning that every request field exists on the record. CI is green and coverage is at 100%. I verified in the working tree that all claimed fixes are present and that all in-tree OAuthClientInformationFull constructors supply client_id.
OAuthClientInformationFull— the client's parse of the authorization server's Dynamic Client Registration response — inherited fromOAuthClientMetadata, the request the client sends. That typed the response as though it had to be a request this SDK would send, so any registered value the server substituted became aValidationErroron a 2xx. This splits the two into siblings so the record parses what a server may legitimately echo.Motivation and Context
We received a report of an authorization server whose registration response returned an
application_typevalue outside{"web", "native"}. The server answered 201 and provisioned the client; the SDK then raisedInvalid registration response: … Input should be 'web' or 'native' [type=literal_error]out of the auth flow, discarding a registration whoseclient_idhad already been minted (and orphaning it server-side on every retry).application_typeis never read again anywhere in the SDK — it's a purely cosmetic field killing an otherwise-valid registration.This isn't specific to that field. The response model is-a request model, so every tight type on the request re-arms the same failure on the response. The same closed-typing had already been widened piecemeal for
*_supportedmetadata,grant_types,response_types, and empty-string URLs — this is the fifth instance of one class, and the class is the inheritance.Spec. RFC 7591 §3.2.1: the server "MAY reject or replace any of the client's requested metadata values submitted during the registration and substitute them with suitable values", and "The client or developer can check the values in the response to determine if the registration is sufficient for use" — a substituted value is a judgment call for the client, not a parse failure. The MCP spec (SEP-837) constrains only what the client sends (
MUST specify an appropriate application_type); it says nothing about the response. The server here is also off-spec — OIDC Registration §2 defines onlynative/web— but that doesn't oblige the client to abort a successful registration over a field it doesn't use. Both are true.Cross-SDK. Every other MCP SDK constrains what it sends and parses
application_typepermissively — the TypeScript SDK types itz.string().optional()with a comment giving exactly this reasoning, go/rust/csharp/ruby likewise. This SDK was the only one that fails on it.What changed
OAuthClientMetadata(request) andOAuthClientInformationFull(registered-client record) are now siblings over a sharedOAuthClientMetadataBaseholding only the fields that are genuinely identical between them:application_type="browser"; the MCPMUSTon the wire is unchanged and enforced by the type.application_type/token_endpoint_auth_methodarestr | None(an echoed""reads as absent, matching the existing URL-field coercion),grant_typesislist[str],redirect_urismay be absent or empty.client_idis now required — RFC 7591 §3.2.1 makes it mandatory in the response, and a "registered client" record without one was never meaningful (a body without it should not parse as success).OAuthRegistrationErrornaming the method when the registration completes, before the record is persisted or any interactive authorization begins (per §3.2.1's "sufficient for use" model).prepare_token_authreports the same for a stored/pre-registered record. The recognized set is derived from oneTokenEndpointAuthMethodtype viaget_args, so send-side and recognize-side cannot drift (private_key_jwtremains recognized —PrivateKeyJWTOAuthProvider's refresh path is unaffected).application_type, so the SDK's own AS reported the default in place of a client's"web". The echo is now built from the validated request'smodel_dump(), and a test pins that every request field exists on the record — a field can no longer be silently omitted.except ValidationErrorinhandle_registration_responsecarried# pragma: no cover— the invalid-response branch was asserted unreachable, which is why this shipped untested. It's now covered and chains the cause (raise … from e).How Has This Been Tested?
null/emptyapplication_type, an unimplementedtoken_endpoint_auth_method, extragrant_types, emptyredirect_uris; the request still rejects an off-setapplication_typeand requiresredirect_uris.handle_registration_responsepath (the layer the failure occurred in) with a substituted body, and with a body that isn't client information at all./registerreturning a substituted 201 completes the full flow (tool call succeeds); a 201 assigning an unusable auth method surfacesOAuthRegistrationErrorwith no/authorizeor/tokenrequest and nothing persisted.application_typefor bothwebandnative."confidential",nullredirect URIs, extra grants) now completes with HTTP 200 wheremaindies with the exactliteral_errorabove; a""auth method reads as absent; aclient_secret_jwtassignment fails at registration before any browser round-trip.strict-no-coverclean.Breaking Changes
Yes — documented in
docs/migration.mdalongside the existing SEP-837 entry.OAuthClientInformationFullis no longer a subclass ofOAuthClientMetadata. Code relying onisinstance(info, OAuthClientMetadata), or passing a record where a request is annotated, must reference the record type directly.validate_scope()/validate_redirect_uri()moved with the record (they're what server code calls) and are no longer onOAuthClientMetadata.application_type/token_endpoint_auth_method→str | None,grant_types→list[str],redirect_urisoptional,client_idrequired. Reads are unaffected.ValidationErrorduring parsing.Conformance: no scenario puts these values in a 201 body, and SEP-837 defines no response-side requirement, so a send-strict / parse-tolerant client passes every existing check unchanged.
Types of changes
Checklist
Additional context
Two things I considered and want to name:
invalid_client.OAuthAuthorizationServerProvider.get_client,authorize). The split stops one class short of separating those, which is whyredirect_uriselements stayAnyUrl(the AS compares them) and why hand-built server records lose a couple of constructor invariants they previously borrowed from the request. Requiringclient_idrestores the one that matters; a fuller server-record / client-response separation is the next seam, deliberately left out of this change.AI Disclaimer