Skip to content

fix(auth): keep the OAuth return target through the provider callback - #623

Open
gale-popai wants to merge 1 commit into
iflytek:mainfrom
gale-popai:fix/oauth-return-to
Open

fix(auth): keep the OAuth return target through the provider callback#623
gale-popai wants to merge 1 commit into
iflytek:mainfrom
gale-popai:fix/oauth-return-to

Conversation

@gale-popai

Copy link
Copy Markdown
Contributor

Problem

returnTo is never honoured after an OAuth login. Every browser login lands on
the default target (/dashboard), no matter what returnTo was 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

SkillHubOAuth2AuthorizationRequestResolver calls rememberReturnTo(request)
on every resolve(...), including the calls where the delegate returned
null. OAuth2AuthorizationRequestRedirectFilter runs the resolver on every
request passing through the chain, so this happens constantly — and
rememberReturnTo removes the session attribute whenever the request has no
returnTo parameter.

The provider callback (/login/oauth2/code/{provider}) is one of those
requests, and the redirect filter sits ahead of OAuth2LoginAuthenticationFilter
in the chain. So the stored target is erased moments before
OAuth2LoginSuccessHandler reads it, and consumeReturnTo always sees null.

Reproducing it against a running instance

The failure handler reads the same attribute, which makes the state observable
without completing a real login:

# store returnTo, then fail the callback deliberately
curl -sc jar "$BASE/oauth2/authorization/google?returnTo=%2Fdevice" -o /dev/null
curl -sb jar -o /dev/null -w '%{http_code} %{redirect_url}\n' \
  "$BASE/login/oauth2/code/google?code=bogus&state=bogus"
# 401 — returnTo was already gone

# same request, but with returnTo on the callback itself
curl -sb jar -o /dev/null -w '%{http_code} %{redirect_url}\n' \
  "$BASE/login/oauth2/code/google?code=bogus&state=bogus&returnTo=%2Fdevice"
# 302 .../login?returnTo=%2Fdevice — the resolver wrote it during the callback

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 — rememberReturnTo used request.getSession(),
which created one on every request just to remove an attribute.

Tests

Added resolve_keepsReturnToOnNonAuthorizationRequests, which walks the
authorization 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 this
path was uncovered.

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 FenjuFu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

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.

2 participants