Skip to content

feat: SSO Rework — Test Login, emailClaim, discoveryUri, protocol selector#27336

Draft
aji-aju wants to merge 29 commits intomainfrom
feat/sso-rework-test-login
Draft

feat: SSO Rework — Test Login, emailClaim, discoveryUri, protocol selector#27336
aji-aju wants to merge 29 commits intomainfrom
feat/sso-rework-test-login

Conversation

@aji-aju
Copy link
Copy Markdown
Collaborator

@aji-aju aji-aju commented Apr 14, 2026

Summary

SSO configuration rework — simplifies setup from 8 provider options to 3 protocols (OIDC/SAML/LDAP), adds Test Login for claim verification, and prevents user lockouts.

Closes #27312

Changes

Backend

  • Schema: Add optional discoveryUri and emailClaim to authenticationConfiguration
  • Email claim priority: jwtPrincipalClaimsMapping (highest) → emailClaim (new) → jwtPrincipalClaims (legacy fallback). Explicit config hard-fails if claim missing; legacy fallback is soft (zero regression)
  • Auto-sync on save: syncFieldsFromDiscoveryUri() — derives authority, publicKeyUrls, clientId from discovery document
  • Test Login API: TestLoginHandler with OIDC popup flow (initiate → IdP auth → callback → extract claims → postMessage) and LDAP inline test
  • SAML email attribute: Check email attribute before NameID fallback (backward compatible)

Frontend

  • Protocol selector: 3 cards (OIDC/SAML/LDAP) replacing 8 provider cards
  • Auth Mode toggle: "With Client Secret (recommended)" / "Without Client Secret" — replaces Public/Confidential jargon
  • Test Login button + Claim Selector: Popup authenticates with IdP, shows all claims, admin picks email claim → auto-fills emailClaim, principalDomain, adminPrincipals
  • Field visibility: Hide auto-derived fields (authority, publicKeyUrls, tokenValidationAlgorithm, legacy claim fields)

Backward Compatibility

  • All new fields are optional — existing configs without them use fallback behavior
  • SAML falls back to NameID if email attribute not present
  • Legacy jwtPrincipalClaims soft fallback still works for unconfigured setups

Test plan

  • Test OIDC Test Login popup flow with Google/Azure/Okta
  • Test claim selection and emailClaim auto-fill
  • Test SAML email attribute extraction
  • Test LDAP inline test login
  • Test auth mode toggle (with/without secret)
  • Verify existing SSO configs work without changes after upgrade
  • Test discoveryUri auto-sync (authority, publicKeyUrls derived)

🤖 Generated with Claude Code


Summary by Gitar

  • System architecture:
    • Enabled vector embedding search index filtering in SystemRepository by checking semanticSearchEnabled status.
  • UI/UX improvements:
    • Updated SSOConfigurationForm click event listeners to use capture phase for better interaction handling.
    • Adjusted CSS layout for service documentation panels in SSOConfigurationForm.tsx.
  • Localization:
    • Added comprehensive i18n keys for new features including workflow management, asset filters, and approval thresholds across multiple language files.

This will update automatically on new commits.

…ocol selector (#27312)

Backend:
- Add discoveryUri and emailClaim fields to authenticationConfiguration schema
- Add emailClaim priority chain in SecurityUtil (claimsMapping > emailClaim > fallback)
- Add syncFieldsFromDiscoveryUri auto-sync on save (authority, clientId, callbackUrl)
- Add TestLoginHandler with OIDC popup flow (initiate + callback) and LDAP inline test
- Add Test Login endpoints in SystemResource (initiate, callback, LDAP)
- Add SAML email attribute check before NameID fallback
- Exclude test-login callback from JwtFilter

Frontend:
- Add protocol selector (OIDC / SAML / LDAP) replacing 8 provider cards
- Add OIDC provider dropdown options with Discovery URI templates
- Add TestLoginButton component (popup + postMessage listener)
- Add ClaimSelector component (claims table + email claim selection)
- Add AuthModeWidget replacing Public/Confidential jargon
- Hide auto-derived fields (authority, publicKeyUrls, tokenValidationAlgorithm)
- Wire Test Login into SSOConfigurationForm

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

Comment on lines +252 to +266
String json = JsonUtils.pojoToJson(message);

resp.setContentType("text/html; charset=UTF-8");
resp.getWriter()
.write(
"<!DOCTYPE html><html><body>"
+ "<p>"
+ (success
? "Authentication successful. This window will close."
: "Error: " + error)
+ "</p>"
+ "<script>"
+ "if (window.opener) {"
+ " window.opener.postMessage("
+ json
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: XSS via IdP claims injected raw into HTML <script> tag

In sendPostMessage(), the JSON-serialized claims from the IdP are embedded directly into a <script> tag without HTML escaping (line 266). Jackson's writeValueAsString() does not escape HTML characters like </script>. If an IdP returns a claim value containing </script><script>alert(document.cookie)</script>, it will break out of the script context and execute arbitrary JavaScript.

Similarly, in sendError() (line 312-317), the message parameter is interpolated into both an HTML <p> tag (no escaping) and a JavaScript string (only single-quote escaping via replace("'", "\'")). Error messages from exceptions may contain attacker-influenced content.

Since the callback endpoint is excluded from JWT authentication (JwtFilter line 109), any user completing an IdP flow reaches this code path.

Suggested fix:

Escape the JSON before embedding in HTML by replacing `</` with `<\/` (standard script-safe encoding). For sendError, HTML-encode the message for the <p> tag and properly escape it for JS:

// In sendPostMessage:
String safeJson = json.replace("</", "<\/");
// use safeJson in the script tag

// In sendError, HTML-encode for the <p> tag:
String htmlSafe = message.replace("&","&amp;").replace("<","&lt;").replace(">","&gt;");
// And for JS string, also escape backslashes, quotes, newlines:
String jsSafe = message.replace("\","\\").replace("'","\'").replace("
","\n");

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

Comment on lines +718 to +731
@GET
@Path("/config/auth/test-login/initiate")
@Produces("text/html")
@Consumes("*/*")
@Operation(
operationId = "testLoginInitiate",
summary = "Initiate Test Login",
description =
"Initiates a Test Login flow by redirecting to the IdP. "
+ "Opens in a popup window. After authentication, redirects to the callback endpoint.")
public void testLoginInitiate(
@Context HttpServletRequest request, @Context HttpServletResponse response) {
TestLoginHandler.handleInitiate(request, response);
}
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: Test login initiate endpoint lacks admin authorization check

The testLoginInitiate endpoint (GET /config/auth/test-login/initiate) requires JWT authentication (not in EXCLUDED_ENDPOINTS), but performs no role-based authorization. Any authenticated user — not just admins — can initiate the test login flow. This is inconsistent with the LDAP test login endpoint which calls authorizer.authorizeAdmin(securityContext). The test login flow is an admin-only diagnostic feature and should be restricted accordingly.

Note: The callback endpoint must remain unauthenticated (it's an IdP redirect), but the initiate endpoint should require admin privileges.

Suggested fix:

Add SecurityContext and admin authorization to the initiate endpoint:

public void testLoginInitiate(
    @Context SecurityContext securityContext,
    @Context HttpServletRequest request,
    @Context HttpServletResponse response) {
  authorizer.authorizeAdmin(securityContext);
  TestLoginHandler.handleInitiate(request, response);
}

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

@gitar-bot
Copy link
Copy Markdown

gitar-bot Bot commented Apr 14, 2026

Code Review 🚫 Blocked 2 resolved / 4 findings

SSO rework integrates emailClaim, discoveryUri, and protocol selection, but is blocked by a critical XSS vulnerability in claims injection and an authorization bypass in the test login endpoint.

🚨 Security: XSS via IdP claims injected raw into HTML <script> tag

📄 openmetadata-service/src/main/java/org/openmetadata/service/security/auth/TestLoginHandler.java:252-266 📄 openmetadata-service/src/main/java/org/openmetadata/service/security/auth/TestLoginHandler.java:305-319 📄 openmetadata-service/src/main/java/org/openmetadata/service/security/auth/TestLoginHandler.java:136-141

In sendPostMessage(), the JSON-serialized claims from the IdP are embedded directly into a <script> tag without HTML escaping (line 266). Jackson's writeValueAsString() does not escape HTML characters like </script>. If an IdP returns a claim value containing </script><script>alert(document.cookie)</script>, it will break out of the script context and execute arbitrary JavaScript.

Similarly, in sendError() (line 312-317), the message parameter is interpolated into both an HTML <p> tag (no escaping) and a JavaScript string (only single-quote escaping via replace("'", "\'")). Error messages from exceptions may contain attacker-influenced content.

Since the callback endpoint is excluded from JWT authentication (JwtFilter line 109), any user completing an IdP flow reaches this code path.

Suggested fix
Escape the JSON before embedding in HTML by replacing `</` with `<\/` (standard script-safe encoding). For sendError, HTML-encode the message for the <p> tag and properly escape it for JS:

// In sendPostMessage:
String safeJson = json.replace("</", "<\/");
// use safeJson in the script tag

// In sendError, HTML-encode for the <p> tag:
String htmlSafe = message.replace("&","&amp;").replace("<","&lt;").replace(">","&gt;");
// And for JS string, also escape backslashes, quotes, newlines:
String jsSafe = message.replace("\","\\").replace("'","\'").replace("
","\n");
⚠️ Security: Test login initiate endpoint lacks admin authorization check

📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/system/SystemResource.java:718-731 📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/system/SystemResource.java:748-762

The testLoginInitiate endpoint (GET /config/auth/test-login/initiate) requires JWT authentication (not in EXCLUDED_ENDPOINTS), but performs no role-based authorization. Any authenticated user — not just admins — can initiate the test login flow. This is inconsistent with the LDAP test login endpoint which calls authorizer.authorizeAdmin(securityContext). The test login flow is an admin-only diagnostic feature and should be restricted accordingly.

Note: The callback endpoint must remain unauthenticated (it's an IdP redirect), but the initiate endpoint should require admin privileges.

Suggested fix
Add SecurityContext and admin authorization to the initiate endpoint:

public void testLoginInitiate(
    @Context SecurityContext securityContext,
    @Context HttpServletRequest request,
    @Context HttpServletResponse response) {
  authorizer.authorizeAdmin(securityContext);
  TestLoginHandler.handleInitiate(request, response);
}
✅ 2 resolved
Edge Case: Stale closure over isLoading/status in popup close checker

📄 openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/TestLogin/TestLoginButton.component.tsx:124-138 📄 openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/TestLogin/TestLoginButton.component.tsx:100-114
In TestLoginButton.component.tsx, the handleTestLogin callback depends on isLoading and status (line 140), but the setInterval closure at line 124-139 captures stale values of these variables. When the interval fires after the popup closes, isLoading and status will always reflect their values at the time handleTestLogin was called (both will be true and 'idle' respectively), not their current state. This means the error status will always be set even if the postMessage handler already processed a successful result.

The 1-second delay (line 128) partially mitigates this but creates a race condition rather than a proper fix.

Bug: Removed @produces("text/html") causes content-type conflict

📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/system/SystemResource.java:717-731
The @Produces("text/html") and @Consumes("*/*") annotations were removed from the testLoginInitiate and testLoginCallback endpoints. These methods now inherit the class-level @Produces(MediaType.APPLICATION_JSON) from SystemResource.

This has two consequences:

  1. Content negotiation mismatch: JAX-RS uses @Produces for routing. A browser navigating to the initiate URL sends Accept: text/html, .... Without a method-level @Produces("text/html"), the framework may not match this endpoint correctly, or it may override the explicit text/html type set on the Response object with the declared application/json—behavior varies by JAX-RS implementation (Jersey vs RESTEasy).
  2. Redirect endpoint: handleInitiate returns a 307 redirect. Some JAX-RS implementations may attempt to serialize the empty redirect body as JSON, adding an unwanted Content-Type: application/json header.

The previous code had @Produces("text/html") for a reason—these endpoints serve HTML pages (the callback) and issue redirects (the initiate), not JSON.

🤖 Prompt for agents
Code Review: SSO rework integrates emailClaim, discoveryUri, and protocol selection, but is blocked by a critical XSS vulnerability in claims injection and an authorization bypass in the test login endpoint.

1. 🚨 Security: XSS via IdP claims injected raw into HTML <script> tag
   Files: openmetadata-service/src/main/java/org/openmetadata/service/security/auth/TestLoginHandler.java:252-266, openmetadata-service/src/main/java/org/openmetadata/service/security/auth/TestLoginHandler.java:305-319, openmetadata-service/src/main/java/org/openmetadata/service/security/auth/TestLoginHandler.java:136-141

   In `sendPostMessage()`, the JSON-serialized claims from the IdP are embedded directly into a `<script>` tag without HTML escaping (line 266). Jackson's `writeValueAsString()` does not escape HTML characters like `</script>`. If an IdP returns a claim value containing `</script><script>alert(document.cookie)</script>`, it will break out of the script context and execute arbitrary JavaScript.
   
   Similarly, in `sendError()` (line 312-317), the `message` parameter is interpolated into both an HTML `<p>` tag (no escaping) and a JavaScript string (only single-quote escaping via `replace("'", "\'")`). Error messages from exceptions may contain attacker-influenced content.
   
   Since the callback endpoint is excluded from JWT authentication (JwtFilter line 109), any user completing an IdP flow reaches this code path.

   Suggested fix:
   Escape the JSON before embedding in HTML by replacing `</` with `<\/` (standard script-safe encoding). For sendError, HTML-encode the message for the <p> tag and properly escape it for JS:
   
   // In sendPostMessage:
   String safeJson = json.replace("</", "<\/");
   // use safeJson in the script tag
   
   // In sendError, HTML-encode for the <p> tag:
   String htmlSafe = message.replace("&","&amp;").replace("<","&lt;").replace(">","&gt;");
   // And for JS string, also escape backslashes, quotes, newlines:
   String jsSafe = message.replace("\","\\").replace("'","\'").replace("
   ","\n");

2. ⚠️ Security: Test login initiate endpoint lacks admin authorization check
   Files: openmetadata-service/src/main/java/org/openmetadata/service/resources/system/SystemResource.java:718-731, openmetadata-service/src/main/java/org/openmetadata/service/resources/system/SystemResource.java:748-762

   The `testLoginInitiate` endpoint (GET `/config/auth/test-login/initiate`) requires JWT authentication (not in EXCLUDED_ENDPOINTS), but performs no role-based authorization. Any authenticated user — not just admins — can initiate the test login flow. This is inconsistent with the LDAP test login endpoint which calls `authorizer.authorizeAdmin(securityContext)`. The test login flow is an admin-only diagnostic feature and should be restricted accordingly.
   
   Note: The callback endpoint must remain unauthenticated (it's an IdP redirect), but the initiate endpoint should require admin privileges.

   Suggested fix:
   Add SecurityContext and admin authorization to the initiate endpoint:
   
   public void testLoginInitiate(
       @Context SecurityContext securityContext,
       @Context HttpServletRequest request,
       @Context HttpServletResponse response) {
     authorizer.authorizeAdmin(securityContext);
     TestLoginHandler.handleInitiate(request, response);
   }

Options

Display: compact → Showing less information.

Comment with these commands to change:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

…ields

- Add test-login/initiate to JwtFilter EXCLUDED_ENDPOINTS (popup window
  doesn't carry JWT token — endpoint only redirects to IdP, no data exposure)
- Hide authorizer fields: botPrincipals, principalDomain, enforcePrincipalDomain,
  enableSecureSocketConnection, useRolesFromProvider, allowedEmailRegistrationDomains,
  allowedDomains, defaultOAuthRole (these will be auto-filled by Test Login
  or moved to advanced settings)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

…e-validation to Test Login

- Hide emailClaim, discoveryUri from form (set via Test Login, not manual input)
- Hide adminPrincipals, enableSelfSignup, enableAutoRedirect (auto-filled or advanced)
- Test Login now validates configuration before opening popup — calls
  validateSecurityConfiguration first, shows error inline if validation fails,
  only opens popup if config is valid

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

The 404 was caused by Dropwizard/Jersey rejecting void return type
for @get methods. Rewritten to return proper JAX-RS Response objects:
- handleInitiate returns Response.temporaryRedirect(authUrl) instead
  of HttpServletResponse.sendRedirect
- handleCallback returns Response.ok(html, "text/html") instead of
  writing to HttpServletResponse directly
- Removed HttpServletResponse dependency from endpoints

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

Moved prompt to hidden in all provider-specific UI schemas
(Google, Azure, Okta, Auth0/Cognito, Custom OIDC) — this is
an advanced setting that rarely needs user configuration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

Root cause of 404: ConfigResource owns @path("/v1/system/config") and
SystemResource is at @path("/v1/system"). When the test-login endpoints
were in SystemResource at @path("/config/auth/test-login/..."), Jersey
routed them to ConfigResource (which owns /config/*) instead. ConfigResource
had no matching sub-path, so it returned 404.

Fix: moved all test-login endpoints to ConfigResource where they belong
under the /config/auth path prefix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

@aji-aju aji-aju self-assigned this Apr 14, 2026
…rm data

TestLoginHandler no longer depends on AuthenticationCodeFlowHandler
being initialized. Instead, it:

1. Reads discoveryUri, clientId, clientSecret, scope from query params
   (passed by frontend from the unsaved form data)
2. Fetches OIDC discovery document directly via OIDCProviderMetadata.resolve()
3. Builds authorization URL from the discovery metadata
4. On callback, builds token request directly using Nimbus SDK
   (ClientSecretBasic for confidential, ClientID-only for public)
5. Stores clientId/secret/discoveryUri in HTTP session for the callback

This means Test Login works BEFORE saving — user fills fields, clicks
Test Login to validate, then saves. No dependency on server state.

Frontend: TestLoginButton now passes form data (discoveryUri, clientId,
clientSecret, scope) as query params to the initiate endpoint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

…ery URI

resolve() expects an Issuer (base URL) and appends .well-known/openid-configuration.
We have the full discovery URI, so use parse() with direct HTTP fetch instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

…_mismatch

Root cause: Test Login used a separate callback URL (/api/v1/system/config/
auth/test-login/callback) that wasn't registered in the IdP's OAuth app.
Google (and other IdPs) reject unregistered redirect URIs with 400.

Fix: Reuse the same /callback URL that's already registered in the IdP.
Differentiate Test Login callbacks using state prefix "test-login:".

Changes:
- TestLoginHandler.handleInitiate(): uses registered callbackUrl (from form
  data or defaults to /callback), prefixes state with "test-login:"
- AuthCallbackServlet.doGet(): checks state prefix, routes test-login
  callbacks to TestLoginHandler.handleCallback()
- Removed separate /auth/test-login/callback endpoint from ConfigResource
- Removed callback from JwtFilter excluded endpoints (servlet handles it)
- Frontend passes callbackUrl from form data to initiate endpoint

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

…ailClaim set

Bug 1: "Client ID is required" on first click — formData was missing from
useCallback dependency array, causing stale closure. Added formData to deps.

Bug 2: Save button now disabled for new OIDC setups until emailClaim is
confirmed via Test Login → Claim Selector flow. Existing configs can
still save freely.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

@github-actions
Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

The JSON schema default was updated but 6 hardcoded fallbacks in
frontend code still used "openid email profile" without offline_access.

Updated:
- SSOUtils.ts (2 occurrences)
- AuthProvider.util.ts (3 occurrences)
- TestLoginButton.component.tsx (1 occurrence)
- conf/openmetadata.yaml (OIDC_SCOPE default)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

…uthMethod to visible

1. Popup communication: replaced window.opener.postMessage with
   localStorage. window.opener is null after redirect chains (initiate
   → IdP → callback). localStorage works across same-origin windows
   regardless of redirects. Popup stores result, parent reads via
   storage event + close-check fallback. Popup always closes itself.

2. Restored scope, prompt to visible in all 5 provider UI schemas
   (they were hidden but should be in advanced config = visible).

3. Restored Okta clientAuthenticationMethod to visible.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

…veryUri

discoveryUri is a top-level field in authenticationConfiguration — the
single source of truth for both public and confidential flows. The
nested oidcConfiguration.discoveryUri is synced from it on save.

- Made top-level discoveryUri visible with title and placeholder
- Hidden nested oidcConfiguration.discoveryUri in all 6 provider
  schemas (OIDC, Google, Azure, Okta, Auth0/Cognito, Custom OIDC)
- One discoveryUri field visible regardless of client type

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

…estLoginButton

handleOidcProviderChange was writing discoveryUri to the nested
oidcConfiguration.discoveryUri (hidden), not the top-level
discoveryUri (visible). TestLoginButton was also checking nested
first. Fixed both to use the top-level field as primary.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

… default

Test Login now mirrors the actual OIDC flow by passing:
- prompt (consent/login/none)
- maxAge
- clientAuthenticationMethod (client_secret_basic/client_secret_post)

Backend: TestLoginHandler reads these from query params, applies to
auth URL, and uses correct client auth method for token exchange.

Also reverted offline_access from default scope — back to
"openid email profile" everywhere (schema, SSOUtils, AuthProvider.util,
TestLoginButton, openmetadata.yaml).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

…normalization

- backend: add normalizeForPersistence and hydrateForResponse in SystemRepository.
  deterministic priority-based mirroring between root and nested OIDC fields,
  always-overwrite derivation of authority and publicKeyUrls from discoveryUri,
  Azure tenant extraction across commercial/gov/china cloud variants.
- backend: SystemResource calls normalizeForPersistence on validate/PUT,
  hydrateForResponse on GET. Legacy configs without discoveryUri are
  preserved unchanged — zero regression for existing customers.
- ui: mirror canonical and legacy OIDC fields in form onChange so RJSF's
  schema required check passes before hitting the API (workaround only;
  backend handles partial payloads on its own).
- ui: validation state machine — Validate button posts to
  /validate?context=testLogin, enables Test Login on success. Save gated
  on Test Login + claim-selector confirm. Reset only when actual
  validation inputs change, not post-test fields.
- ui: hide Azure tenant (backend-derived). Advanced Configuration accordion
  collapses non-essential OIDC fields via SSO_ADVANCED_OIDC_FIELDS.
- tests: SystemRepositoryNormalizeTest covers mirror directions, Azure
  tenant extraction (all cloud variants), legacy preservation, null safety,
  no-network-call on hydrate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

adminPrincipals is often configured by email (e.g. "alice@company.com")
while the derived userName is typically the email local part ("alice").
The previous check only looked up userName in adminPrincipals, so a user
configured by email never got promoted to admin on login.

- SecurityUtil: new isAdminPrincipal(adminPrincipals, userName, email)
  helper that matches either form.
- AuthenticationCodeFlowHandler: use helper on existing and new OIDC
  user paths.
- SamlAuthServletHandler: same for existing and new SAML user paths.
- SamlAssertionConsumerServlet: same for JWT token admin flag.

LDAP intentionally unchanged — LDAP principals are usernames, not emails.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

Two coupled changes enabling the full Test Login gated flow across both
OIDC and SAML:

## SAML Test Login

- New TestSamlHandler builds a temporary Saml2Settings from form-provided
  IdP details, redirects popup to IdP with RelayState="saml-test-login:<uuid>",
  and on callback verifies the SAMLResponse + flattens attributes into the
  same {success, claims} payload that OIDC Test Login produces.
- New POST /v1/system/config/auth/test-login/saml-initiate endpoint using
  @FormParam so Jersey reliably parses the form-urlencoded body.
- AuthCallbackServlet.doPost now routes SAML Test Login callbacks by
  RelayState prefix (parallel to the existing doGet state-prefix routing
  for OIDC Test Login).
- TestLoginResponses extracted from TestLoginHandler so both OIDC and SAML
  use the same popup HTML / localStorage handoff.
- UI: TestLoginButton branches by provider; SAML uses a hidden-form POST
  with target=_blank so the cert fits cleanly in the body.
- UI: SAML main form now shows only IdP Entity ID, SSO Login URL, X.509
  Certificate, NameID Format. SP fields are surfaced in a banner and
  derived server-side. SP certs, signing flags, and security options move
  to an Advanced Configuration accordion via SSO_ADVANCED_SAML_FIELDS.
- ClaimSelector handles multi-valued SAML attributes (arrays joined for
  display, single values stringified as before).

## OIDC validator refinement

- New SystemRepository.validateDiscoveryUriReachable runs at the top of
  validateSecurityConfiguration for any OIDC provider with a discoveryUri.
  Short-circuits with a single clear error ("Could not reach Discovery
  URI") when the URI is unreachable, invalid JSON, missing issuer/jwks_uri,
  or (for Azure) doesn't match the Azure AD URL shape.
- Removed duplicate discoveryUri fetches from Azure, Okta, Auth0, and
  Cognito validators now that the upstream check handles reachability.
  Each validator keeps only its provider-specific semantic checks (issuer
  format, required endpoints, client credential validation).
- Improved error messages where fields are backend-derived:
  Azure: "Tenant could not be determined from Discovery URI…"
  Okta/Auth0/Cognito: "domain/URL could not be verified…"
  Custom OIDC: "Discovery document is missing required endpoints…"

## Validation state machine (shared across OIDC and SAML)

- areMainFieldsFilled and didValidationInputsChange now branch on
  provider so SAML uses IdP fields and OIDC uses discoveryUri + client
  credentials. validationStatus resets only when fields that actually
  affect validation change.
- Test Login and Validate button gates now accept either OIDC or SAML.

## Tests

8 new tests in SystemRepositoryNormalizeTest for the upstream discoveryUri
check (unreachable, invalid JSON, missing issuer, Azure shape mismatch,
LDAP scope skip, valid happy path, invalid URL format, legacy config
fall-through).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

+ "</script>"
+ "</body></html>";

return Response.ok(html, "text/html").build();
+ "</script>"
+ "</body></html>";

return Response.status(Response.Status.BAD_REQUEST).entity(html).type("text/html").build();
+ "</script>"
+ "</body></html>";

return Response.ok(html, "text/html").build();
+ "</script>"
+ "</body></html>";

return Response.status(Response.Status.BAD_REQUEST).entity(html).type("text/html").build();
- TestLoginHandler: PKCE code_challenge sent by default (unless
  disablePkce=true), code_verifier stored in session and included in
  token exchange. Nonce now conditional on useNonce param. customParams
  parsed from JSON and merged into auth URL. Session cleanup includes
  CODE_VERIFIER. clientAuthMethod extracted to constant.
- TestSamlHandler: replaced HttpSession storage with ConcurrentHashMap
  keyed by RelayState UUID. Fixes "Session expired" error caused by
  SameSite=Lax blocking cookies on IdP's cross-origin POST callback.
  Entries auto-cleaned after 5 min TTL.
- SystemRepository: normalizeForPersistence now populates serverUrl
  (derived from callbackUrl). discoveryUri mirror changed to one-way
  root→nested only (Option B) — legacy configs with only nested
  discoveryUri no longer trigger derivation on unrelated PATCHes.
  Removed dead autoPopulatePublicKeyUrlsIfNeeded method.
- SystemResource: PATCH handler calls normalizeForPersistence after
  patch apply. PUT handler @Valid removed (fires before normalize,
  rejects partial payloads that normalize would fill).
- TestLoginButton: passes disablePkce, useNonce, customParams to
  backend for OIDC Test Login.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

Switch OIDC Test Login initiate from GET+query-params to POST+form-body
so clientSecret stays out of URLs, server logs, and browser history.

- ConfigResource: endpoint changed to POST with @FormParam for each
  OIDC field (same pattern as SAML initiate).
- TestLoginHandler: accepts params as method arguments instead of
  req.getParameter() (Jersey consumes form body before handler runs).
- Redirect changed from 307 (temporaryRedirect) to 302 so the browser
  converts POST→GET when following the redirect to the IdP. 307
  preserved the POST method, causing Azure to reject with
  "client_id missing in body".
- TestLoginButton: OIDC now uses hidden-form POST with target=_blank
  (same pattern as SAML). Both protocols use identical transport.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

LDAP pre-save Test Login so admins can verify form-provided LDAP config
without requiring a saved authenticator. UI collects user credentials
via an inline modal (no popup — LDAP has no external IdP to redirect
to). ClaimSelector is skipped since LDAP's email attribute is explicit;
principalDomain and adminPrincipal are auto-filled from the test user's
resolved email.

## Backend

- TestLdapHandler: stateless handler that accepts form-provided
  LdapConfiguration + user credentials. Admin bind → search by
  mailAttributeName → user bind → read mail attribute. Uses UnboundID
  LDAPConnection directly; no singletons, no DB writes, no user
  creation. Try-with-resources for connection lifecycle.
- ConfigResource: POST /v1/system/config/auth/test-login/ldap-initiate
  (JSON body with ldapConfiguration, email, password). Inline
  LdapTestLoginRequest DTO.
- JwtFilter: whitelisted the new endpoint.
- LdapAuthenticator: admin promotion now checks both userName and
  email via SecurityUtil.isAdminPrincipal — matches the fix already
  applied to OIDC/SAML. Fixes the case where adminPrincipals is saved
  as a full email but LDAP login was comparing against the local-part
  username.

## Frontend

- TestLoginButton: LDAP branch opens an Ant Design Modal with email
  and password inputs. Submits JSON to /ldap-initiate, calls onSuccess
  with derivedPrincipalDomain + suggestedAdminPrincipal. No popup.
- SSOConfigurationForm: handleTestLoginSuccess branches on provider —
  LDAP bypasses ClaimSelector entirely and directly applies
  principalDomain + adminPrincipals (emailClaim is left unset so the
  jwtPrincipalClaims fallback picks up "email" from the OM-issued JWT).
  areMainFieldsFilled and didValidationInputsChange handle LDAP fields.
- TestLogin.interface: ldapConfiguration added to form data type.

## Tests (44 new, 67 total)

- TestLoginHandlerTest (17) — OIDC validation branches, callback
  validation, buildClientAuthentication helper (basic/post/fallback),
  buildTestLoginResult (timestamp filtering, email detection, domain
  derivation, null handling).
- TestSamlHandlerTest (14) — SAML validation branches, RelayState
  routing, buildSamlSettings (IdP fields, NameID format, security
  flags off), buildClaimsFromAuth (single/multi-value flattening,
  empty/null handling).
- TestLdapHandlerTest (13) — uses UnboundID InMemoryDirectoryServer
  (real LDAP in-process). Covers input validation, happy path, wrong
  admin password, user not found, wrong user password, wrong
  userBaseDN, wrong mailAttributeName, server unreachable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

…login

# Conflicts:
#	openmetadata-ui/src/main/resources/ui/src/locale/languages/ar-sa.json
#	openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json
#	openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json
#	openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json
#	openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json
#	openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json
#	openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json
#	openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json
#	openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json
#	openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json
#	openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json
#	openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json
#	openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json
#	openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json
#	openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json
#	openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json
#	openmetadata-ui/src/main/resources/ui/src/locale/languages/tr-tr.json
#	openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json
#	openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-tw.json
@github-actions
Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

Formatting-only changes: import reordering and long argument list splitting per Google Java Format. No logic changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

siddhant1 pushed a commit that referenced this pull request Apr 29, 2026
Remove AuthModeWidget, TestLogin components/types, and net-new SSO
util/API additions from PR #27336 so this branch carries only the
backend Test Login support and inherits our existing SSO chrome work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
siddhant1 pushed a commit that referenced this pull request Apr 29, 2026
- Wire SSO Test Login to PR #27336's backend: TestLoginButton
  (OIDC/SAML hidden-form-POST popup + LDAP credential modal) and a
  ClaimSelector modal that auto-populates adminPrincipal (email
  local-part) and principalDomain from the chosen email claim. Drop the
  old broken call to /security/config/test.
- Make oidcConfiguration.secret optional with a recommended hint;
  auto-derive clientType from secret presence; hide the manual radio.
- Drop adminPrincipals + principalDomain from authorizerConfiguration
  required[] so initial save isn't blocked. Hide them during the
  new-config flow; keep main tier so they surface once Test Login fills.
- Strip oidcConfiguration from the schema for SAML/LDAP to stop RJSF
  materializing defaults inside a hidden subtree.
- Hide root-level OIDC mirrors and tenant per the field-layout
  proposal; surface preferredJwsAlgorithm in advanced; remove in-form
  callbackUrl (the read-only display beside the form is the source).
- Fix TestLoginButton formData prop shape so SAML/LDAP click paths
  route correctly; vertically center action-bar buttons.
- Sync 19 new i18n keys across the 17 locales.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

SSO Rework: Simplify to 3 protocols (OIDC/SAML/LDAP) with Test Login validation

2 participants