Skip to content

Improvements to SSO#27446

Open
harshach wants to merge 4 commits intomainfrom
sso_improvements
Open

Improvements to SSO#27446
harshach wants to merge 4 commits intomainfrom
sso_improvements

Conversation

@harshach
Copy link
Copy Markdown
Collaborator

Describe your changes:

Fixes

I worked on ... because ...

Type of change:

  • Bug fix
  • Improvement
  • New feature
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation

Checklist:

  • I have read the CONTRIBUTING document.
  • My PR title is Fixes <issue-number>: <short explanation>
  • I have commented on my code, particularly in hard-to-understand areas.
  • For JSON Schema changes: I updated the migration scripts or explained why it is not needed.

@harshach harshach requested a review from akash-jain-10 as a code owner April 17, 2026 00:24
Copilot AI review requested due to automatic review settings April 17, 2026 00:24
@harshach harshach requested review from a team and tutte as code owners April 17, 2026 00:24
@github-actions github-actions bot added backend safe to test Add this label to run secure Github workflows on PRs labels Apr 17, 2026
Comment on lines 456 to +458
public static void checkAndStoreRedirectUriInSession(HttpSession session, String redirectUri) {
checkAndStoreRedirectUriInSession(session, redirectUri, new String[0]);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Security: No-args overload of checkAndStoreRedirectUriInSession skips validation

The 2-argument overload checkAndStoreRedirectUriInSession(session, redirectUri) at line 456 delegates to the varargs version with an empty array. Because the varargs version guards validation behind trustedBaseUrls.length > 0 (line 466-467), any caller using the 2-arg overload stores the redirect URI in the session without any open-redirect validation. This defeats the purpose of the new RedirectUriValidator. If this overload is kept for backward compatibility, it should either always validate or be removed/deprecated so callers are forced to supply trusted URLs.

Suggested fix:

Either remove the 2-arg overload entirely, or make the varargs version reject the URI when no trusted URLs are provided:

  if (trustedBaseUrls == null || trustedBaseUrls.length == 0
      || !RedirectUriValidator.isSafe(redirectUri, trustedBaseUrls)) {
    LOG.warn("[OIDC] Rejecting redirect URI — no allowlist or not in allowlist");
    throw new TechnicalException("Redirect URI is not in the allowlist");
  }

Was this helpful? React with 👍 / 👎 | Reply gitar fix to apply this suggestion

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 17, 2026

❌ UI Checkstyle Failed

❌ ESLint + Prettier + Organise Imports (src)

One or more source files have linting or formatting issues.

Affected files
  • openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchUtils.test.tsx
    • openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchUtils.tsx

Fix locally (fast — only checks files changed in this branch):

make ui-checkstyle-changed

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to harden SSO/SAML flows (open-redirect/XSS protections, stricter SAML defaults) and includes additional, seemingly unrelated ingestion integration tests.

Changes:

  • UI: Remove dangerouslySetInnerHTML highlighting in advanced search dropdown labels; build mention suggestion DOM nodes safely in FeedEditor.
  • Service: Introduce RedirectUriValidator and apply redirect allowlist checks in SAML and OIDC login/callback flows; store SAML AuthnRequest ID and validate it on callback.
  • Spec/config: Flip SAML security defaults (strictMode, wantMessagesSigned, wantAssertionsSigned) to true; update example YAMLs accordingly.
  • Ingestion: Add Fivetran mock-server based integration tests.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchUtils.tsx Replaces HTML-string highlight with React nodes (removes dangerouslySetInnerHTML).
openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchUtils.test.tsx Updates tests for React-node highlighting and adds XSS regression coverage.
openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/FeedEditor/FeedEditor.tsx Avoids string-built HTML for mention items by using DOM APIs and textContent.
openmetadata-spec/src/main/resources/json/schema/security/client/samlSSOClientConfig.json Makes SAML security defaults stricter (potentially breaking).
openmetadata-service/src/main/java/org/openmetadata/service/security/saml/SamlLoginServlet.java Adds redirect allowlist validation and stores SAML request ID in session.
openmetadata-service/src/main/java/org/openmetadata/service/security/auth/SamlAuthServletHandler.java Adds redirect allowlist validation + request ID validation on callback.
openmetadata-service/src/main/java/org/openmetadata/service/security/RedirectUriValidator.java New allowlist validator for post-auth redirect URIs.
openmetadata-service/src/main/java/org/openmetadata/service/security/AuthenticationCodeFlowHandler.java Adds allowlist enforcement for OIDC redirect URIs and reduces sensitive logging.
openmetadata-service/src/main/java/org/openmetadata/service/security/jwt/JWTTokenGenerator.java Sets token type based on user.isBot instead of always treating as BOT.
openmetadata-service/src/main/java/org/openmetadata/service/security/DefaultAuthorizer.java Ensures impersonation authorization is checked for admin/admin-or-bot endpoints too.
openmetadata-service/src/test/java/org/openmetadata/service/security/auth/SamlAuthServletHandlerTest.java Updates SAML handler tests for new redirect validation/session usage.
openmetadata-service/src/test/java/org/openmetadata/service/security/AuthenticationCodeFlowHandlerTest.java Adjusts expected redirect URI in a test.
conf/openmetadata.yaml Updates sample SAML security defaults to true.
docker/development/distributed-test/local/server1.yaml Updates sample SAML security defaults to true.
docker/development/distributed-test/local/server2.yaml Updates sample SAML security defaults to true.
docker/development/distributed-test/local/server3.yaml Updates sample SAML security defaults to true.
ingestion/tests/integration/fivetran/conftest.py Adds mock HTTP server + fixtures for Fivetran integration tests.
ingestion/tests/integration/fivetran/test_fivetran_client.py Adds Fivetran client integration tests (pagination, HVA scenario, etc.).
ingestion/tests/integration/fivetran/init.py Marks the new integration test directory as a package.

Comment on lines +11 to +19
"""
Fivetran integration tests using a mock HTTP server
"""
import pytest

from metadata.generated.schema.entity.services.connections.pipeline.fivetranConnection import (
FivetranConnection,
)
from metadata.ingestion.source.pipeline.fivetran.client import FivetranClient
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

This file introduces new Fivetran ingestion integration tests, which appears unrelated to the PR’s stated focus (“Improvements to SSO”). Please split these changes into a separate PR (or update the PR title/description to reflect the additional scope) to keep review, release notes, and rollback surface area manageable.

Copilot uses AI. Check for mistakes.
Comment on lines +67 to +70
String trustedCallback = SamlSettingsHolder.getInstance().getRelayState();
String baseRequestUrl = buildBaseRequestUrl(request);
if (RedirectUriValidator.isSafe(redirectUri, trustedCallback, baseRequestUrl)) {
session.setAttribute(SESSION_REDIRECT_URI, redirectUri);
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

SamlSettingsHolder.getInstance() returns a new holder instance each call, and relayState is only set inside initDefaultSettings() on a different instance. As a result getInstance().getRelayState() will always be null here, so the allowlist check effectively falls back to buildBaseRequestUrl(request) only (and may allow unintended redirects). Use the configured SAML callback/relayState from the active auth config (or make relayState static/singleton) instead of reading it from a fresh instance.

Copilot uses AI. Check for mistakes.
Comment on lines 124 to +127
if (callbackUrl != null) {
req.getSession(true).setAttribute(SESSION_REDIRECT_URI, callbackUrl);
String trustedCallback = SamlSettingsHolder.getInstance().getRelayState();
if (RedirectUriValidator.isSafe(callbackUrl, trustedCallback, buildBaseRequestUrl(req))) {
session.setAttribute(SESSION_REDIRECT_URI, callbackUrl);
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

Using buildBaseRequestUrl(req) (derived from the incoming request host/scheme) as an allowlisted base URL is unsafe: the Host header can be attacker-controlled, which can make an external callbackUrl appear “same-origin” and bypass the redirect allowlist. This should validate against a configured/public base URL (e.g., serverUrl / configured SAML callback origin), not a value constructed from the request.

Copilot uses AI. Check for mistakes.
Comment on lines +125 to +126
String trustedCallback = SamlSettingsHolder.getInstance().getRelayState();
if (RedirectUriValidator.isSafe(callbackUrl, trustedCallback, buildBaseRequestUrl(req))) {
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

SamlSettingsHolder.getInstance().getRelayState() will always be null because getInstance() constructs a new holder and relayState is only set during initDefaultSettings() on another instance. This means the allowlist is not actually anchored to the configured relayState/callback. Please fetch relayState from the configured SAML settings (or refactor SamlSettingsHolder to retain relayState) before relying on it for redirect validation.

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +79
String scheme = uri.getScheme();
String host = uri.getHost();
if (scheme == null
|| host == null
|| !ALLOWED_SCHEMES.contains(scheme.toLowerCase(Locale.ROOT))) {
LOG.warn("Rejecting redirect URI with unsupported scheme or missing host");
return false;
}
for (String base : allowedBaseUrls) {
if (CommonUtil.nullOrEmpty(base)) {
continue;
}
try {
URI baseUri = new URI(base);
if (baseUri.getHost() == null) {
continue;
}
if (scheme.equalsIgnoreCase(baseUri.getScheme())
&& host.equalsIgnoreCase(baseUri.getHost())
&& uri.getPort() == baseUri.getPort()) {
return true;
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

Port matching uses uri.getPort() == baseUri.getPort(), which treats an omitted port as -1. This will incorrectly reject equivalent URLs like https://host/path (port -1) vs https://host:443/path (port 443), and similarly for http/80. Normalize -1 to the scheme’s default port (or compare effective ports) before deciding allowlist membership.

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +87
public static boolean isSafe(String candidate, String... allowedBaseUrls) {
if (CommonUtil.nullOrEmpty(candidate)) {
return false;
}
if (candidate.startsWith("/") && !candidate.startsWith("//")) {
return true;
}
URI uri;
try {
uri = new URI(candidate);
} catch (URISyntaxException e) {
LOG.warn("Rejecting malformed redirect URI");
return false;
}
String scheme = uri.getScheme();
String host = uri.getHost();
if (scheme == null
|| host == null
|| !ALLOWED_SCHEMES.contains(scheme.toLowerCase(Locale.ROOT))) {
LOG.warn("Rejecting redirect URI with unsupported scheme or missing host");
return false;
}
for (String base : allowedBaseUrls) {
if (CommonUtil.nullOrEmpty(base)) {
continue;
}
try {
URI baseUri = new URI(base);
if (baseUri.getHost() == null) {
continue;
}
if (scheme.equalsIgnoreCase(baseUri.getScheme())
&& host.equalsIgnoreCase(baseUri.getHost())
&& uri.getPort() == baseUri.getPort()) {
return true;
}
} catch (URISyntaxException ignored) {
// Try next base URL.
}
}
LOG.warn("Rejecting redirect URI (host not in allowlist)");
return false;
}
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

RedirectUriValidator is a new security-critical utility but there are no unit tests covering key cases (e.g., absolute URL allowlisted vs rejected, default-port normalization, malformed URIs, scheme-relative // URLs). Adding focused unit tests would prevent regressions and ensure the open-redirect protections behave as intended.

Copilot uses AI. Check for mistakes.
Comment on lines 77 to +115
@@ -107,12 +107,12 @@
"wantMessagesSigned": {
"description": "SP requires the messages received to be signed.",
"type": "boolean",
"default": false
"default": true
},
"wantAssertionsSigned": {
"description": "SP requires the assertions received to be signed.",
"type": "boolean",
"default": false
"default": true
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

Changing the default values of strictMode, wantMessagesSigned, and wantAssertionsSigned from false to true is a behavior change that can break existing SAML setups that relied on the old defaults. This should be called out explicitly in the PR description (and potentially treated as a backward-incompatible change / documented in release notes), otherwise operators may be surprised by failed SSO after upgrade.

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Copy Markdown
Contributor

The Python checkstyle failed.

Please run make py_format and py_format_check in the root of your repository and commit the changes to this PR.
You can also use pre-commit to automate the Python code formatting.

You can install the pre-commit hooks with make install_test precommit_install.

@github-actions
Copy link
Copy Markdown
Contributor

Jest test Coverage

UI tests summary

Lines Statements Branches Functions
Coverage: 63%
63.71% (59604/93547) 43.65% (31366/71857) 46.74% (9426/20166)

@gitar-bot
Copy link
Copy Markdown

gitar-bot bot commented Apr 17, 2026

Code Review ⚠️ Changes requested 3 resolved / 4 findings

Refactors SSO redirect logic by centralizing buildBaseRequestUrl and improving validator test coverage, but the no-args checkAndStoreRedirectUriInSession override must be secured to prevent validation bypass.

⚠️ Security: No-args overload of checkAndStoreRedirectUriInSession skips validation

📄 openmetadata-service/src/main/java/org/openmetadata/service/security/AuthenticationCodeFlowHandler.java:456-458 📄 openmetadata-service/src/main/java/org/openmetadata/service/security/AuthenticationCodeFlowHandler.java:466-471

The 2-argument overload checkAndStoreRedirectUriInSession(session, redirectUri) at line 456 delegates to the varargs version with an empty array. Because the varargs version guards validation behind trustedBaseUrls.length > 0 (line 466-467), any caller using the 2-arg overload stores the redirect URI in the session without any open-redirect validation. This defeats the purpose of the new RedirectUriValidator. If this overload is kept for backward compatibility, it should either always validate or be removed/deprecated so callers are forced to supply trusted URLs.

Suggested fix
Either remove the 2-arg overload entirely, or make the varargs version reject the URI when no trusted URLs are provided:

  if (trustedBaseUrls == null || trustedBaseUrls.length == 0
      || !RedirectUriValidator.isSafe(redirectUri, trustedBaseUrls)) {
    LOG.warn("[OIDC] Rejecting redirect URI — no allowlist or not in allowlist");
    throw new TechnicalException("Redirect URI is not in the allowlist");
  }
✅ 3 resolved
Security: RedirectUriValidator has no dedicated unit tests

📄 openmetadata-service/src/main/java/org/openmetadata/service/security/RedirectUriValidator.java:45-59
The new RedirectUriValidator class is a security-critical component protecting against open-redirect attacks, but it has no dedicated test class. The existing SAML/OIDC handler tests only exercise it indirectly with relative paths. Edge cases like protocol-relative URLs (//evil.com), paths with backslashes (/\evil.com), encoded slashes (/%2f%2fevil.com), fragment/query tricks, and URIs with userinfo (/foo@evil.com) should be explicitly covered to prevent regressions.

Security: SAML callback re-validates redirectUri but silently falls back on failure

📄 openmetadata-service/src/main/java/org/openmetadata/service/security/auth/SamlAuthServletHandler.java:263-277
In SamlAuthServletHandler.handleCallback, when the stored redirect URI fails re-validation (line 264-267), the code silently falls back to /auth/callback?id_token=... and appends the JWT token. While this prevents the open-redirect, the session was already poisoned with the URI during handleLogin. If an attacker can race or manipulate session state between login and callback, the mismatch between 'store permissively, validate later' creates a fragile trust boundary. In handleLogin the SAML flow already validates (line 126), so the re-validation in callback is defense-in-depth — which is good — but consider also clearing the stored session attribute when it fails validation in callback (line 275-277 only logs).

Quality: Duplicated buildBaseRequestUrl method across two classes

📄 openmetadata-service/src/main/java/org/openmetadata/service/security/auth/SamlAuthServletHandler.java:152-161 📄 openmetadata-service/src/main/java/org/openmetadata/service/security/saml/SamlLoginServlet.java:76-85
The private method buildBaseRequestUrl(HttpServletRequest) is identically implemented in both SamlAuthServletHandler (line 152-161) and SamlLoginServlet (line 76-85). This is a maintenance risk — if one is updated the other may be missed. Consider extracting it to a shared utility (e.g., RedirectUriValidator or a new HttpRequestUtil).

🤖 Prompt for agents
Code Review: Refactors SSO redirect logic by centralizing buildBaseRequestUrl and improving validator test coverage, but the no-args `checkAndStoreRedirectUriInSession` override must be secured to prevent validation bypass.

1. ⚠️ Security: No-args overload of checkAndStoreRedirectUriInSession skips validation
   Files: openmetadata-service/src/main/java/org/openmetadata/service/security/AuthenticationCodeFlowHandler.java:456-458, openmetadata-service/src/main/java/org/openmetadata/service/security/AuthenticationCodeFlowHandler.java:466-471

   The 2-argument overload `checkAndStoreRedirectUriInSession(session, redirectUri)` at line 456 delegates to the varargs version with an empty array. Because the varargs version guards validation behind `trustedBaseUrls.length > 0` (line 466-467), any caller using the 2-arg overload stores the redirect URI in the session **without any open-redirect validation**. This defeats the purpose of the new `RedirectUriValidator`. If this overload is kept for backward compatibility, it should either always validate or be removed/deprecated so callers are forced to supply trusted URLs.

   Suggested fix:
   Either remove the 2-arg overload entirely, or make the varargs version reject the URI when no trusted URLs are provided:
   
     if (trustedBaseUrls == null || trustedBaseUrls.length == 0
         || !RedirectUriValidator.isSafe(redirectUri, trustedBaseUrls)) {
       LOG.warn("[OIDC] Rejecting redirect URI — no allowlist or not in allowlist");
       throw new TechnicalException("Redirect URI is not in the allowlist");
     }

Options

Display: compact → Showing less information.

Comment with these commands to change:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@sonarqubecloud
Copy link
Copy Markdown

@sonarqubecloud
Copy link
Copy Markdown

@github-actions
Copy link
Copy Markdown
Contributor

🔴 Playwright Results — 1 failure(s), 29 flaky

✅ 3651 passed · ❌ 1 failed · 🟡 29 flaky · ⏭️ 89 skipped

Shard Passed Failed Flaky Skipped
🔴 Shard 1 473 1 6 4
🟡 Shard 2 648 0 3 7
🟡 Shard 3 650 0 6 1
🟡 Shard 4 628 0 6 27
🟡 Shard 5 609 0 2 42
🟡 Shard 6 643 0 6 8

Genuine Failures (failed on all attempts)

Pages/SearchIndexApplication.spec.ts › Search Index Application (shard 1)
Error: �[2mexpect(�[22m�[31mreceived�[39m�[2m).�[22mtoEqual�[2m(�[22m�[32mexpected�[39m�[2m) // deep equality�[22m

Expected: �[32mStringMatching /success|activeError/g�[39m
Received: �[31m"failed"�[39m
🟡 29 flaky test(s) (passed on retry)
  • Features/DataAssetRulesDisabled.spec.ts › Verify the ApiEndpoint entity item action after rules disabled (shard 1, 1 retry)
  • Features/DataAssetRulesDisabled.spec.ts › Verify the Chart entity item action after rules disabled (shard 1, 1 retry)
  • Features/DataAssetRulesDisabled.spec.ts › Verify the Database entity item action after rules disabled (shard 1, 1 retry)
  • Features/CustomizeDetailPage.spec.ts › Stored Procedure - customization should work (shard 1, 1 retry)
  • Pages/Customproperties-part1.spec.ts › Integer (shard 1, 1 retry)
  • Pages/UserCreationWithPersona.spec.ts › Create user with persona and verify on profile (shard 1, 1 retry)
  • Features/ActivityFeed.spec.ts › emoji reactions can be added when feed messages exist (shard 2, 1 retry)
  • Features/BulkEditEntity.spec.ts › Glossary (shard 2, 1 retry)
  • Features/ChangeSummaryBadge.spec.ts › Automated badge should appear on entity description with Automated source (shard 2, 1 retry)
  • Features/IncidentManager.spec.ts › Complete Incident lifecycle with table owner (shard 3, 1 retry)
  • Features/RestoreEntityInheritedFields.spec.ts › Validate restore with Inherited domain and data products assigned (shard 3, 1 retry)
  • Features/RestoreEntityInheritedFields.spec.ts › Validate restore with Inherited domain and data products assigned (shard 3, 2 retries)
  • Features/RestoreEntityInheritedFields.spec.ts › Validate restore with Inherited domain and data products assigned (shard 3, 1 retry)
  • Features/RTL.spec.ts › Verify Following widget functionality (shard 3, 1 retry)
  • Features/UserProfileOnlineStatus.spec.ts › Should show "Active recently" for users active within last hour (shard 3, 1 retry)
  • Pages/Customproperties-part2.spec.ts › entityReferenceList shows item count, scrollable list, no expand toggle (shard 4, 1 retry)
  • Pages/DataContracts.spec.ts › Create Data Contract and validate for Store Procedure (shard 4, 1 retry)
  • Pages/DataContractsSemanticRules.spec.ts › Validate Description Rule Is_Not_Set (shard 4, 1 retry)
  • Pages/DataProductAndSubdomains.spec.ts › Add expert to data product via UI (shard 4, 1 retry)
  • Pages/Domains.spec.ts › Domain owner should able to edit description of domain (shard 4, 1 retry)
  • Pages/Domains.spec.ts › Rename domain with subdomains attached verifies subdomain accessibility (shard 4, 1 retry)
  • Pages/EntityDataSteward.spec.ts › DisplayName Add, Update and Remove for child entities (shard 5, 1 retry)
  • Pages/Glossary.spec.ts › Add and Remove Assets (shard 5, 2 retries)
  • Pages/Lineage/LineageFilters.spec.ts › Verify lineage schema filter selection (shard 6, 1 retry)
  • Pages/Lineage/LineageRightPanel.spec.ts › Verify custom properties tab IS visible for supported type: searchIndex (shard 6, 1 retry)
  • Pages/ODCSImportExport.spec.ts › Multi-object ODCS contract - object selector shows all schema objects (shard 6, 1 retry)
  • Pages/UserDetails.spec.ts › Admin user can edit teams from the user profile (shard 6, 1 retry)
  • Pages/Users.spec.ts › Permissions for table details page for Data Consumer (shard 6, 1 retry)
  • VersionPages/EntityVersionPages.spec.ts › Directory (shard 6, 1 retry)

📦 Download artifacts

How to debug locally
# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip    # view trace

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend safe to test Add this label to run secure Github workflows on PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants