Skip to content

fix: deliver the rest of the invalid_request class to the client - #28736

Draft
BobbyHo wants to merge 4 commits into
plat479-4-consolidate-redirectsfrom
plat479-5-deliver-invalid-request
Draft

fix: deliver the rest of the invalid_request class to the client#28736
BobbyHo wants to merge 4 commits into
plat479-4-consolidate-redirectsfrom
plat479-5-deliver-invalid-request

Conversation

@BobbyHo

@BobbyHo BobbyHo commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Stacked on #28450. Review that first; this PR's diff is only the top four commits.

TL;DR

#28450 moved three authorization failures to the client's callback per RFC 6749 §4.1.2.1 and left the largest group behind. A malformed code_challenge, a malformed resource, an unparseable response_type, or an excess parameter still stopped on Coder with no state, so the app never learned its request failed and waited on an authorization that would never arrive. Same defect #28450 exists to fix, on the path #28450 did not touch.

Contract change. These now redirect to the registered callback with error=invalid_request, a description naming the failing fields, and the request's state, on both verbs:

Verb Was
GET static "Invalid Query Parameters" page, 400
POST WriteOAuth2Error, 400

Two failures still answer on Coder, because in both cases the callback is not trustworthy yet: a redirect_uri that does not parse or does not match the registration, and a missing or unparsable client_id.

Precedence change. An app whose registered callback has a rejected scheme now answers 500 even when the request also carries parser errors; that combination used to answer 400. Only reachable for an app row that bypassed registration. No test pinned the old precedence; DangerousCallbackSchemeOutranksParseFailure pins the new one.


Both preconditions move into a constructor (2ec3bdb)

Writing a Location from inside the parser's error branch would run before the scheme check, which is the hazard DangerousCallbackSchemeNotRedirected guards. validatedCallbackURL becomes authorizeResponse, built only by newAuthorizeResponse, which runs the scheme check and the exact match against the registration in that order. Holding one is what licenses a redirect, so the ordering is structural rather than a convention each call site has to remember.

  • The scheme check runs on the registered URL, not on the parser's result: p.RedirectURL returns the client's URI on a mismatch, so checking that would answer 500 and log the app as corrupt for a request the app never made. DangerousClientSchemeIsNotTheServersFault pins it.
  • A mismatch zeroes the callback, so canRedirect() is false for any redirect_uri failure. The type refuses to name a destination it could not validate.
  • state moves onto the type, out of the parameter lists of withQuery, errorURL, codeURL, and redirectAuthorizeError, so no call site can emit a response the client cannot correlate.

Classification replaces implicit ordering (90be3d1)

extractAuthorizeParams returns one authorizeFailure instead of two trailing values, giving both handlers the same three-way decision: bad server state answers 500, a deliverable failure redirects, everything else stays on Coder.

blamesClient tests only client_id, since the redirect-URI carve-out is already structural. That carve-out is reachable in practice, not just defensively: httpmw accepts the RFC 6749 §2.3.1 Basic credential as client_id, so the query parameter can legitimately be empty.

API reference (a68a32d)

Both authorize endpoints reject response_type=token, but the reference listed it as an accepted value. CRF-15 on #28450 asked for this alongside the 302 rows and it was the one part left undone. Enums on the typed parameter appends to the list swaggo derives from the type rather than replacing it, so narrowing it means declaring the parameter as a string. codersdk.OAuth2ProviderResponseType keeps both constants, since it also feeds ResponseTypesSupported.

Left alone on purpose. CRF-26 (the server_error sites) and CRF-13 (fragment delivery), both as in #28450.

Refs PLAT-479.

…onstruction

The redirect URI a response goes to had to pass two checks: an exact match
against the app's registration, and a scheme check. The match ran inside
extractAuthorizeParams, the scheme check separately in each handler afterwards,
so the ordering that keeps an unchecked scheme out of a Location header was a
convention two call sites had to keep rather than a property of the value.

newAuthorizeResponse runs both, and is the only way to get a callback. The
scheme is checked on the registered URL rather than on the match's result,
because the parser returns the client's URI when the match fails and a 500 there
would blame the app for a request it did not make.

state moves onto the response, so no call site can build a §4.1.2.1 error the
client cannot correlate by passing "".

extractAuthorizeParams now returns one authorizeFailure instead of a slice and
an error, giving both handlers an explicit three-way decision.

Precedence change: an app whose registered callback has a rejected scheme now
answers 500 even when the request also has parser errors, where that
combination previously answered 400.
…lass

extractAuthorizeParams discarded a callback it had already exact-matched and
returned nothing on any parser error, so a malformed code_challenge, a
malformed resource, an unparseable response_type, or an excess parameter
stopped on Coder. GET rendered "Invalid Query Parameters", POST wrote a JSON
400, and neither carried state. The app never learned its request failed and
waited on an authorization that would never arrive.

RFC 6749 §4.1.2.1 delivers these to the client's own callback. Only two
failures stay here: one naming the redirect URI, where the response has no
destination because the URI never matched, and one naming the client
identifier, where the registration the callback was matched against may not
belong to whoever is asking.

The redirect is safe in the same way #28450's is: the destination has passed
the exact match and the scheme check, both now run at construction. Descriptions
quote client input and go through the existing sanitizing chokepoint.

Tests cover both verbs with redirect_uri omitted and sent explicitly, and pin
the combination of a rejected registered scheme with a parser error at 500.
The two entries #28450 added cover one redirected code each. Parameter
validation failures are the larger class and now arrive the same way, so
integrators need to know which ones reach their callback and which two stay on
Coder.
@linear-code

linear-code Bot commented Aug 27, 2026

Copy link
Copy Markdown

PLAT-479

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Docs preview

Check off each page once it's been reviewed. If a page changes in a later push, its checkbox clears automatically so it gets a fresh look. Pages not yet wired into the docs navigation aren't listed here.

CRF-15 asked for this alongside the 302 responses #28450 documented, and it was
the one part left undone. Both authorize endpoints reject token, but the
reference listed it as an accepted value, so a client reading the table sends a
request the endpoint refuses.

Enums on the typed parameter appends to the list swaggo derives from the type
rather than replacing it, yielding code, token, code. Declaring the parameter as
a string is what narrows it. The generated JSON already rendered it as a string
with an inline enum, so only the enum array changes.

codersdk.OAuth2ProviderResponseType keeps both constants: it also feeds
ResponseTypesSupported, which already advertises code alone.
@BobbyHo BobbyHo changed the title fix(coderd/oauth2provider): deliver the rest of the invalid_request class to the client fix: deliver the rest of the invalid_request class to the client Aug 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant