Honour the response_mode a client asks for #648

Merged ngerakines.me
targeting main from oauth-response-mode-fragment
Merged opened by and assigned to ngerakines.me
targeting main from oauth-response-mode-fragment

Thanks to the Colibri developers for the report and for finding the line it came from.

response_mode (OAuth 2.0 Multiple Response Type Encoding Practices 2.1) was not declared on ParInput at all, so serde dropped it. Every pushed authorization request was staged as though it had asked for query, and the consent page built redirect_uri?code=...&state=...&iss=... unconditionally.

@atproto/oauth-client-browser sets responseMode: 'fragment' by default and reads its callback out of location.hash, so every single-page client built on it was returned to its own redirect URI carrying the code in a place it does not look. The browser navigated, the client found nothing, and no error was raised at either end -- sign-in simply never completed, with nothing in a log on either side to say why.

The parameter is now read from both PAR intake shapes, stored on the staged request, and honoured everywhere an authorization response is written. Absent still means query, which is the default for response_type=code and what every request was answered with before.

oauth_par carries it as a nullable column because the mode is decided at PAR and needed at a later request, against a row that may outlive the process. A row staged before the column existed reads back as query -- not an absent answer, but the mode it was actually answered in, which matters for the flows in flight across an upgrade.

Two paths write the response and both were changed. The consent page assembles the parameters once and then either sets them on the fragment or on the query, reading the mode the server settled at PAR rather than guessing at the client. The delegated callback is the one place this server builds the redirect itself, in a 303; its success and its access_denied now go through the same builder, which also retires delegation::redirect_with in favour of one renderer for both modes.

form_post is refused at PAR rather than downgraded. The reference server offers it and this one cannot: it would need this origin to render an auto-submitting form aimed at the client, and HTML_CSP carries form-action 'self', so the browser would block the submission and the flow would die on a blank page. Accepting it and answering with a query would reproduce exactly the failure above. response_modes_supported is built from the same constant PAR validates against, so the metadata document cannot promise a mode the endpoint refuses.

Nine tests hold it: the PAR-to-authorize fragment path, the unstated default, the form_post refusal naming the mode, the advertised list, the round trip through both the consuming and non-consuming reads of the SQL store, the consent script's fragment branch, and a delegated redirect landing in the fragment with the client's own query component untouched.

Commits 0