Skip to content

fix: SSO login redirect lands on /applications instead of original app URL#41820

Open
sebastianiv21 wants to merge 5 commits into
releasefrom
claude/admiring-pike-ff92e9
Open

fix: SSO login redirect lands on /applications instead of original app URL#41820
sebastianiv21 wants to merge 5 commits into
releasefrom
claude/admiring-pike-ff92e9

Conversation

@sebastianiv21
Copy link
Copy Markdown
Contributor

@sebastianiv21 sebastianiv21 commented May 15, 2026

Description

Fixes a regression introduced in #41550 where unauthenticated users opening a deep app link (e.g. https://<host>/app/<app-name>/<page-id>?ssoTrigger=saml) land on /applications after completing OIDC/SAML login instead of the originally requested URL.

Root cause

The frontend triggers the SSO entry URL via window.location.href = "/oauth2/authorization/keycloak?redirectUrl=<encoded absolute URL>" — a top-level GET navigation. Browsers do not send the Origin header for top-level GET navigations, but the redirectUrl value is always absolute (it comes from window.location.href).

RedirectHelper.isSafeRedirectUrl reads httpHeaders.getOrigin(), finds it empty, and returns false for any absolute URL. sanitizeRedirectUrl then rewrites the URL to <origin>/applications, which gets baked into the OAuth2 state and used by the post-callback success handler — so the user always lands on /applications.

Fix

Restructure isSafeRedirectUrl so the early bypass-blocking checks (relative path, http(s) prefix, URI parse, userInfo, null host) happen first, then split into two host-comparison branches:

  • Origin present: existing logic — full host + port + scheme-aware port normalization. Unchanged semantics.
  • Origin absent: derive the request host from X-Forwarded-Host (preferred, supports proxied envs and comma-separated lists) then Host. Compare hostnames only (these headers carry no scheme/port). If neither header is present, reject — preserves the old strict behavior in the truly no-info case.

The open-redirect protection from #41550 is preserved — only same-host URLs become permissible in the no-Origin path, and userinfo / protocol-relative / non-http(s) bypass attempts are still rejected.

Fixes appsmith-ee#8815

Automation

/ok-to-test tags="@tag.All"

🔍 Cypress test results

Tip

🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
Workflow run: https://github.com/appsmithorg/appsmith/actions/runs/26175316808
Commit: d37c496
Cypress dashboard.
Tags: @tag.All
Spec:


Wed, 20 May 2026 21:54:33 UTC

Communication

Should the DevRel and Marketing teams inform users about this change?

  • Yes
  • No

Test plan

  • Unit tests added in RedirectHelperOpenRedirectTest covering the new fallback path: Host / X-Forwarded-Host, XFH precedence, comma-separated XFH lists, port stripping, IPv6 brackets, userinfo + protocol-relative bypass attempts blocked.
  • On a Keycloak/OIDC-enabled instance: sign out, open a deep app link in incognito, complete SSO, verify the browser lands on /app/<app-name>/<page-id>?ssoTrigger=saml (not /applications).
  • Negative check: visit /oauth2/authorization/keycloak?redirectUrl=https://evil.com/phish, complete SSO, verify the browser lands on /applications and server logs contain Blocked open redirect attempt to: https://evil.com/phish.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Hardened redirect validation to block malformed or userinfo-containing URLs, require a resolved request host, and enforce origin/host/port matching with scheme-aware default-port handling and IPv6/port parsing; improved logging sanitization for rejected redirects.
  • Tests

    • Added tests covering Origin-absent fallbacks, X-Forwarded-Host precedence (first value), comma/IPv6/port parsing, X-Forwarded-Port behavior, and negative cases for unsafe redirects.

Review Change Stack

…p URL (#8815)

PR #41550 made isSafeRedirectUrl reject any absolute redirect URL when the
Origin header is absent. Browsers do not send Origin on top-level GET
navigations, which is exactly how the frontend triggers the SSO entry URL
(window.location.href = "/oauth2/authorization/...?redirectUrl=<absolute>").
The absolute redirectUrl was therefore replaced with /applications and baked
into the OAuth2 state, so post-SSO the user always landed on /applications
rather than the deep app link they opened.

Relax the no-Origin path to accept same-host absolute URLs, validated against
X-Forwarded-Host (preferred, for proxied envs) or Host. Hostname-only
comparison since these headers carry no scheme/port. The open-redirect
protection from #41550 is preserved — only same-host URLs become permissible
when Origin is absent, and userinfo / protocol-relative / non-http(s) bypass
attempts are still rejected.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions github-actions Bot added the Bug Something isn't working label May 15, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 15, 2026

Walkthrough

This PR tightens open-redirect checks by parsing redirect URIs, rejecting userinfo/malformed targets, enforcing Origin-based host/port matching when present, and adding an Origin-absent fallback that derives request host/port from X-Forwarded-Host/Host with IPv6 and port normalization.

Changes

Origin-absent redirect fallback validation

Layer / File(s) Summary
Redirect URL validation refactoring and documentation
app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/RedirectHelper.java
Updated isSafeRedirectUrl Javadoc and refactored absolute-URL validation to parse redirect URIs, reject userinfo, require a non-null redirect host, and handle Origin-present host/port-aware matching.
Origin-absent fallback implementation
app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/RedirectHelper.java
Added InetSocketAddress import and implemented fallback path with extractRequestHost (prefers X-Forwarded-Host first value), stripPort, extractRequestPort, and portOf helpers; normalizes IPv6 brackets, strips/derives ports, and compares host/port after scheme-default normalization.
Logging sanitization
app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/RedirectHelper.java
Added sanitizeForLog and changed blocked-redirect logging to use sanitized/truncated values while preserving fallback redirect construction.
OAuth resolver delegation
app/server/appsmith-server/src/main/java/com/appsmith/server/authentication/handlers/ce/CustomServerOAuth2AuthorizationRequestResolverCE.java
extractHostFromRequest now delegates header-based host resolution to RedirectHelper.extractRequestHost(...) and falls back to the request URI host when needed.
Test coverage for Origin-absent fallback
app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/RedirectHelperOpenRedirectTest.java
Added InetSocketAddress import and tests verifying X-Forwarded-Host precedence, comma-separated parsing (first value), port handling (including implicit/default ports and X-Forwarded-Port), IPv6 bracket stripping, and blocking of unsafe authorities and protocol-relative URLs.

🎯 4 (Complex) | ⏱️ ~45 minutes


Suggested reviewers:

  • sondermanish

🔐 A redirect now finds its way home,
When Origin takes a little roam,
X-Forwarded-Host steps in with grace,
IPv6 brackets fall into place,
Logs stay tidy, safe paths find their trace. 🛡️

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main regression fix: SSO login redirects now land on the original app URL instead of /applications.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description is comprehensive, well-structured, and addresses all key template sections including root cause analysis, fix explanation, test plan, and communication decision.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/admiring-pike-ff92e9

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sebastianiv21 sebastianiv21 changed the title fix: SSO login redirect lands on /applications instead of original app URL (#8815) fix: SSO login redirect lands on /applications instead of original app URL May 15, 2026
@sebastianiv21 sebastianiv21 added the ok-to-test Required label for CI label May 15, 2026
@sebastianiv21
Copy link
Copy Markdown
Contributor Author

/build-deploy-preview skip-tests=true

@github-actions
Copy link
Copy Markdown

Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/25946297135.
Workflow: On demand build Docker image and deploy preview.
skip-tests: true.
env: ``.
PR: 41820.
recreate: .
base-image-tag: .

@github-actions
Copy link
Copy Markdown

Deploy-Preview-URL: https://ce-41820.dp.appsmith.com

Copy link
Copy Markdown
Collaborator

@subrata71 subrata71 left a comment

Choose a reason for hiding this comment

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

The changes look good to me. If you want to address those nitpicks then it would be great otherwise I can approve. But before you merge this can you please create a shadow EE PR and make sure all tests pass there as well?

* a comma-separated list — the first entry is the outermost client-facing host),
* then falls back to the Host header. Returns null when neither is set.
*/
private static String extractRequestHost(HttpHeaders headers) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could you please check if there are already such util method available which does the same thing? Just trying to make sure we are not keeping duplicate logic scattered in different places.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

CustomServerOAuth2AuthorizationRequestResolverCE.extractHostFromRequest was doing essentially the same job (XFH comma-parsing + Host fallback) in the same SSO entry flow.

I promoted RedirectHelper.extractRequestHost(HttpHeaders) from private to public static and refactored the OAuth2 resolver to delegate.

The resolver still keeps request.getURI().getHost() as a last-resort fallback, so the only behavior change is the order between Host header and URI host when XFH is unset (which is a no-op in proxied envs since XFH is always set there). Let me know if the changes are ok.

sebastianiv21 and others added 2 commits May 19, 2026 19:56
- Add warn logs in the two URI parse catch blocks in isSafeRedirectUrl
  (one for redirectUrl, one for Origin header) so malformed inputs are
  visible in server logs instead of silently rejected.
- Extract sanitizeForLog(String) to centralize the CR/LF strip + 200-char
  truncate previously inlined in sanitizeRedirectUrl; reuse it in the two
  new logs.
- Promote RedirectHelper.extractRequestHost(HttpHeaders) from private to
  public static. Refactor CustomServerOAuth2AuthorizationRequestResolverCE
  .extractHostFromRequest to delegate to it, removing the duplicate XFH /
  Host header parsing. The request URI host is preserved as a last-resort
  fallback in the resolver.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/RedirectHelper.java (1)

246-256: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve the request port in the Origin-absent validation path.

This branch now reduces the request side to hostname-only, so a request for app.example.com will also allow https://app.example.com:8443/... whenever Origin is missing. That is a different origin, and it can bounce the browser to another service on the same host after login. Please carry the forwarded/host port through when it is present and compare it against redirectUri.getPort() instead of dropping it unconditionally.

Also applies to: 271-283

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/RedirectHelper.java`
around lines 246 - 256, The current Origin-absent path in isSameOrigin drops the
request port by using extractRequestHost(httpHeaders) and comparing only
hostname to redirectHost (after normalizing brackets), which permits
different-origin requests on nonstandard ports; update isSameOrigin to preserve
and parse the forwarded/request port from httpHeaders (e.g., Host or
X-Forwarded-Host/Port) alongside extractRequestHost, build a request host:port
representation, and compare the request port against redirectUri.getPort() (and
hostname against normalizedRedirectHost) instead of ignoring the port; adjust
the logic in the same block (and the similar block around lines 271-283) to
treat IPv6 bracketed hosts the same but include port comparison when present.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/RedirectHelper.java`:
- Around line 246-256: The current Origin-absent path in isSameOrigin drops the
request port by using extractRequestHost(httpHeaders) and comparing only
hostname to redirectHost (after normalizing brackets), which permits
different-origin requests on nonstandard ports; update isSameOrigin to preserve
and parse the forwarded/request port from httpHeaders (e.g., Host or
X-Forwarded-Host/Port) alongside extractRequestHost, build a request host:port
representation, and compare the request port against redirectUri.getPort() (and
hostname against normalizedRedirectHost) instead of ignoring the port; adjust
the logic in the same block (and the similar block around lines 271-283) to
treat IPv6 bracketed hosts the same but include port comparison when present.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b6d5d0da-615d-494f-b69b-9dacd38c3ae8

📥 Commits

Reviewing files that changed from the base of the PR and between 5cfb648 and 4063c03.

📒 Files selected for processing (2)
  • app/server/appsmith-server/src/main/java/com/appsmith/server/authentication/handlers/ce/CustomServerOAuth2AuthorizationRequestResolverCE.java
  • app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/RedirectHelper.java

@github-actions
Copy link
Copy Markdown

Failed server tests

  • com.appsmith.server.helpers.UserUtilsTest#makeInstanceAdministrator_WhenUserAlreadyAdmin_MaintainsPermissionsSuccessfully

1 similar comment
@github-actions
Copy link
Copy Markdown

Failed server tests

  • com.appsmith.server.helpers.UserUtilsTest#makeInstanceAdministrator_WhenUserAlreadyAdmin_MaintainsPermissionsSuccessfully

Addresses a same-host open-redirect vector flagged in review: the
Origin-absent fallback in isSafeRedirectUrl compared hostnames only, so a
request to app.example.com would also allow https://app.example.com:8443/...
(a different service on the same host) after login.

Now the fallback path also compares ports. The request port is derived from
X-Forwarded-Port, a port embedded in X-Forwarded-Host, or the Host header
(mirroring extractRequestHost's source precedence). Since these headers carry
no scheme, an implicit request port is normalized to the default for the
redirect URL's scheme via the existing normalizePort helper. Same-host
redirects to a different port are rejected; default-vs-explicit-default ports
still match.

Adds tests for the new port comparison and updates the prior
X-Forwarded-Host-with-port test to assert port matching.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/RedirectHelper.java`:
- Around line 317-339: The extractRequestPort method currently ignores
X-Forwarded-Port unless X-Forwarded-Host is present; change it to first check
and parse X-Forwarded-Port (headers.getFirst("X-Forwarded-Port")) independently
and return its parsed value if valid, then fall back to X-Forwarded-Host parsing
(using portOf(first) as now) and finally to headers.getHost() as before; ensure
you handle comma-separated lists and NumberFormatException the same way and
preserve the existing return values (-1 when unknown).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bd20574f-ebbd-4242-a835-02314de8e9f3

📥 Commits

Reviewing files that changed from the base of the PR and between 4063c03 and f148c92.

📒 Files selected for processing (2)
  • app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/RedirectHelper.java
  • app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/RedirectHelperOpenRedirectTest.java

Per review, extractRequestPort previously only consulted X-Forwarded-Port
inside the X-Forwarded-Host branch. Proxies that preserve the Host header but
convey the client-facing port solely via X-Forwarded-Port would have their
port ignored, causing same-host redirects on non-default ports to be rejected
in the Origin-absent flow.

Read X-Forwarded-Port first (regardless of X-Forwarded-Host), then a port
embedded in X-Forwarded-Host, then the Host header port. Adds a regression
test for the Host + X-Forwarded-Port combination.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@sebastianiv21 sebastianiv21 requested a review from subrata71 May 22, 2026 00:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Something isn't working ok-to-test Required label for CI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants