fix(auth): keep the OAuth return target through the provider callback - #623
fix(auth): keep the OAuth return target through the provider callback#623gale-popai wants to merge 1 commit into
Conversation
OAuth2AuthorizationRequestRedirectFilter invokes the resolver on every request in the chain and the delegate answers null for anything that is not an authorization request. Recording the return target on those calls cleared it again on the next request without a returnTo parameter — the provider callback included, which this filter processes before login succeeds. The success handler therefore always found an empty session attribute and fell back to the default target, so returnTo never worked. Guard the write on a non-null authorization request. As a side effect, anonymous API requests no longer allocate a session via getSession(). Signed-off-by: Gal Eyal <gal.e@popai.health>
FenjuFu
left a comment
There was a problem hiding this comment.
LGTM. Correct diagnosis and the right fix — OAuth2AuthorizationRequestRedirectFilter invokes the resolver on every request in the chain, so the previous unconditional rememberReturnTo re-cleared the stored target on the next request, including the provider callback (which carries no returnTo and is processed before the success handler reads it). Gating on authorizationRequest != null scopes the write to actual authorization requests, and the new test captures exactly that callback-preserves-returnTo path.
Security-wise I checked the open-redirect surface: rememberReturnTo sanitizes via OAuthLoginRedirectSupport.sanitizeReturnTo on write (line 79) and the read path sanitizes again (line 94), so a hostile returnTo can't turn this into an open redirect. Good.
Problem
returnTois never honoured after an OAuth login. Every browser login lands onthe default target (
/dashboard), no matter whatreturnTowas passed to/oauth2/authorization/{provider}.This breaks any deep link that goes through login — in our case the device
authorization page, where the user signs in and is then dropped on the
dashboard instead of returning to approve the pending code.
Cause
SkillHubOAuth2AuthorizationRequestResolvercallsrememberReturnTo(request)on every
resolve(...), including the calls where the delegate returnednull.OAuth2AuthorizationRequestRedirectFilterruns the resolver on everyrequest passing through the chain, so this happens constantly — and
rememberReturnToremoves the session attribute whenever the request has noreturnToparameter.The provider callback (
/login/oauth2/code/{provider}) is one of thoserequests, and the redirect filter sits ahead of
OAuth2LoginAuthenticationFilterin the chain. So the stored target is erased moments before
OAuth2LoginSuccessHandlerreads it, andconsumeReturnToalways seesnull.Reproducing it against a running instance
The failure handler reads the same attribute, which makes the state observable
without completing a real login:
The second call succeeding is what pins the cause: the resolver is writing
session state on a request that is not an authorization request.
Fix
Only record the return target when the delegate actually produced an
authorization request. As a side effect, anonymous API requests no longer
allocate an HTTP session —
rememberReturnTousedrequest.getSession(),which created one on every request just to remove an attribute.
Tests
Added
resolve_keepsReturnToOnNonAuthorizationRequests, which walks theauthorization request and then the callback over the same session and asserts
the target survives. The existing tests only exercised the two-argument
overload on a matching path, where the delegate never returns
null, so thispath was uncovered.