Skip to content

Handle refused and unknown authorizations in the callback - #45

Merged
robertlemke merged 1 commit into
mainfrom
task/callback-robustness
Sep 14, 2026
Merged

robertlemke merged 1 commit into
mainfrom
task/callback-robustness

Conversation

@robertlemke

Copy link
Copy Markdown
Member

Makes the callback of the authorization code flow robust against the cases which browsers and authorization servers produce in practice.

When the OAuth server refuses an authorization, for example because the user denied access, the browser now returns to the application with the error code in the query parameter flownative_oauth2_error_{serviceType}. States which are malformed, unknown or already used result in status 400 instead of a server error.

The token request repeats the redirect URI which was stored with the authorization request, and the client credentials grant sends none. Rendering the redirect URI outside of a web request without Neos.Flow.http.baseUri fails with a clear exception. The granted scope comes from the token response instead of the callback URL, and a failed token request ends in an OAuthClientException.

Breaking changes: finishAuthorization() no longer takes a scope argument, createOAuthProvider() has an additional optional argument, and renderFinishAuthorizationUri() may throw an OAuthClientException.

The callback now returns the browser to the application when the OAuth
server refuses an authorization, and answers states which are malformed,
unknown or already used with status 400. The token request repeats the
redirect URI of the authorization request, and a failed token request
ends in an OAuthClientException.

Previously, the callback required a code. An authorization server which
sent an error instead, for example because the user denied access,
caused a server error. The return URI now carries the error code in the
query parameter flownative_oauth2_error_{serviceType}. Only error codes
which RFC 6749 and OpenID Connect define are passed on, because
applications may display them.

A malformed state made the cache throw, and an unknown or reused state
ended in status 500 with a stack trace in the logs. Both usually come
from a reloaded or bookmarked page, so they now result in status 400.

The redirect URI was rendered again for the token request. If the
browser returned through another host name, the two values differed and
the server rejected the exchange. The URI is now stored with the state.
The client credentials grant no longer sends a redirect URI. Rendering
it outside of a web request without the setting Neos.Flow.http.baseUri
throws a clear exception instead of a TypeError. The putenv() call is
gone, because it changed the environment of the whole process.

The granted scope is taken from the token response. The scope parameter
of the callback URL no longer overrides it, so finishAuthorization()
lost its scope argument. Transport errors and token responses without
JSON are wrapped in an OAuthClientException. The code is no longer
written to the log, because masking it failed for codes shorter than
three characters.
@robertlemke
robertlemke merged commit 03c01ef into main Sep 14, 2026
6 checks passed
@robertlemke
robertlemke deleted the task/callback-robustness branch September 14, 2026 12:52
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