Skip to content

fix(socket): validate credentials on push-feed WebSocket handshakes under all configurations - #36143

Merged
TeddyCr merged 11 commits into
open-metadata:mainfrom
TeddyCr:push-feed-handshake-validation
Oct 29, 2026
Merged

TeddyCr merged 11 commits into
open-metadata:mainfrom
TeddyCr:push-feed-handshake-validation

Conversation

@TeddyCr

@TeddyCr TeddyCr commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Describe your changes:

Push-feed WebSocket handshakes are now authenticated under every configuration:

  • A presented bearer token is always validated against the authentication/authorizer configuration (previously this was gated on the enableSecureSocketConnection authorizer setting).
  • Browsers cannot set headers on a WebSocket upgrade, so the UI's socket.io handshake now passes its token as a token query parameter (required for public-client SSO, which holds an IdP token and no OM_SESSION cookie); without it, those handshakes would be rejected.
  • A session cookie is validated against the session store; a handshake carrying neither a token nor a session cookie is rejected (fail closed), including before the session store is wired.
  • Internally-issued tokens (which carry the session claim) are revalidated against the store; externally-issued tokens have no session claim and are accepted on token validation alone — the same basis their HTTP requests stand on.
  • A validated token binds the socket to the same identity its HTTP requests carry (JwtFilter.resolveUserName, the JwtIdentityResolver path): the stored username resolved from the email when emailClaim is configured, the legacy principal claims otherwise. Previously the handshake resolved the principal with the legacy helper only, so email-first users could authenticate over HTTP yet lose the push feed.
  • The connection handler no longer accepts a socket identity or session from raw query parameters — the handler keys a socket only under the identity and session the handshake filter validated and stamped.
  • Because a presented token must validate, the browser cannot reconnect with a token that expired while the socket was down. The UI provider therefore drives its own bounded reconnect (3 attempts, 1s/2s/4s backoff) that reads the current token before each attempt, instead of socket.io's automatic reconnect whose handshake query is fixed at construction. It also closes the previous socket when the user changes or the provider unmounts.
  • The connection handler and the polling filter strip any client-supplied UserId/SessionId headers before stamping the validated identity, so a validated field that is absent never falls back to a client value; a handshake that yields no user id is rejected with 401.
  • Neither the servlet nor the WebSocket upgrade handler logs the handshake query string (it may now carry the token). Reverse proxies and access logs in front of OpenMetadata should redact the token query parameter on /api/v1/push/feed.

enableSecureSocketConnection on the authorizer configuration no longer has any effect; it is marked deprecated (deprecated: true) in the schema (generated classes and TS types carry the annotation), the dereferenced schemas, and the sample configuration (the field is kept for compatibility — existing YAML continues to load).

The rule is recorded in docs/decisions/2026-10-26-push-feed-handshakes-always-authenticate.md.

This change came out of an internal security review; the specifics are tracked with the security team out-of-band and are intentionally not described here.

Type of change:

  • Bug fix
  • Improvement
  • New feature
  • Breaking change (custom non-UI clients that opened push-feed sockets with no credentials must now present an authenticated session or token; a deployment with no authorizerConfiguration has no validator and no session store, so it can no longer open push-feed sockets and logs a warning at startup)
  • Documentation

High-level design:

SocketAddressFilter is the only path into the push feed, so enforcing credential validation there closes the gap regardless of servlet registration:

  • jwtFilterFor builds the token validator whenever authentication and authorizer configuration are present, independent of the socket-security flag.
  • resolveHandshakeToken prefers the Authorization header and falls back to the token query parameter (WebSocket upgrades cannot set headers); a presented token must validate even when a session cookie rides along.
  • The session check requires a verifiable credential before deciding: token-only handshakes are revalidated against the store when the token carries a session claim, and stand alone when they do not (IdP tokens) or when there is no store yet; cookie handshakes are validated and must agree with the requested identity if one is supplied.
  • validatePrefixedTokenRequest binds the token to JwtFilter.resolveUserName(claims) — the one principal resolution shared with the HTTP filter — rather than reading the legacy principal claims itself.
  • WebSocketManager derives the socket key and session exclusively from the filter-stamped headers. The Jetty upgrade creator runs the same SocketAddressFilter.authenticate (the servlet filter chain never sees a WebSocket upgrade) and stamps the identity into the engine.io connection headers; both paths drop client-supplied UserId/SessionId first.
  • WebSocketProvider sets reconnection: false and schedules its own reconnect: await getOidcToken(), replace socket.io.opts.query, then socket.connect(), so the engine is created only after the current token is in place. socket.io's reconnect_attempt hook fires synchronously right before the engine copies the query, which is too early for an asynchronous token read.

Alternatives considered: defaulting the authorizer flag to true in the schema (still allowed uncredentialed sockets when set false, i.e. the flag remained a foot-gun) and validating in WebSocketManager only (the identity would still be adopted before a credential check could reach it). Both rejected in favour of a single fail-closed gate at the filter.

Tests:

Use cases covered

  • Token-validated handshake binds the socket to the token principal; a mismatched requested user id is rejected with 403 (header and query-param variants).
  • Session-cookie handshake with no requested user id binds to the session owner; a requested user id diverging from the session is rejected with 403.
  • A handshake with neither credential is rejected with 401 and never reaches the connection handler (regression test; fails before the fix).
  • Public-client SSO: query-param token with no session cookie is authenticated under the default configuration; an invalid token is rejected even alongside a valid session cookie; an externally-issued token (no session claim) is accepted on token validity without store validation.
  • Early-startup ordering: cookie without a session store is rejected; a validated token without a store is accepted on its own.
  • Email-first identity: with emailClaim configured and a token carrying only an email claim, the socket binds to the stored username resolved from the email (real JwtFilter; rejected on the previous resolver, verified RED).
  • Browser reconnect (Jest): the token sent on reconnect is the one read at reconnect time, not the one captured at init; 1s/2s/4s backoff and a budget of three attempts; budget reset on a successful connect; io client disconnect / io server disconnect not retried; unmount closes the socket and drops pending reconnects; a setup superseded by a user change while its token read is pending never creates or publishes a socket (all RED on the previous provider).
  • Client-supplied UserId/SessionId upgrade headers (mixed casing, multiple values) are dropped before the validated identity is stamped; an external token with no session claim leaves no SessionId at all (WebSocket handler and polling wrapper, both verified RED before the fix).
  • The handshake token never reaches the log at any level (verified RED: the URI, parameter map and parsed query all carried it before).
  • A cookie session with no stored user id and no requested user id is rejected with 401 instead of accepting a socket that would never be registered.
  • Real-validator regression: the production constructor with enableSecureSocketConnection: false and no injected validator rejects a garbage token= even alongside a valid session cookie (RED against the pre-PR flag gate).
  • MCP: the shared validatePrefixedTokenRequest now resolves the principal through JwtFilter.resolveUserName, so an email-first token authenticates on the MCP bearer path like it does over HTTP (new case in OAuthHttpStatelessServerTransportProviderTest).
  • Existing regression suite for header spoofing + session revalidation (SocketAddressFilterSpoofRevalidationBypassTest) still passes.

Unit tests

  • I added unit tests for the new/changed logic.
  • Files added/updated: openmetadata-service/src/test/java/org/openmetadata/service/socket/SocketAddressFilterTest.java (22 tests), Jetty12WebSocketHandlerTest.java (7), HeaderRequestWrapperTest.java (6), openmetadata-mcp/.../OAuthHttpStatelessServerTransportProviderTest.java (+1), openmetadata-ui/src/main/resources/ui/src/context/WebSocketProvider/WebSocketProvider.test.tsx (8 tests, new)
  • Coverage on changed classes: full branch coverage of the new paths in SocketAddressFilter, Jetty12WebSocketHandler and HeaderRequestWrapper via the socket test suite plus JwtFilterTest (org.openmetadata.service.socket.*Test + JwtFilterTest — 142 tests, all green; OAuthHttpStatelessServerTransportProviderTest — 30, green); the credential-less, email-first, header-stripping, token-logging and real-validator regression tests were verified RED before their fixes and GREEN after.

Backend integration tests

  • Not applicable (no backend API changes; the servlet/filter path is covered by the unit suite).

Ingestion integration tests

  • Not applicable (no ingestion changes).

Playwright (UI) tests

  • Not applicable
  • No new spec: there is no UI surface change (WebSocketProvider adds the handshake token it already holds and drives its own reconnect, covered by the Jest suite above). The full Playwright suite runs in the merge queue, and it is the regression signal for this change: the queue failed at d59475af because WebSocket upgrades lost their stamped identity and the jobs tray never refreshed (import/export and async-job suites); 9002c05 moved handshake validation into the upgrade creator, and the next queue run verifies it.

Manual testing performed

  1. mvn test on the socket test suite of openmetadata-service before the change: the new credential-less-handshake test failed, demonstrating the unauthenticated bind.
  2. Same suite after the change: 142/142 green (credential-less handshake rejected, token-based and cookie-based handshakes unaffected, query-param token path, email-first resolution, header stripping on both transports, token kept out of the log, real-validator rejection with the flag off).
  3. yarn jest src/context/WebSocketProvider — 8/8 green on the new provider, all red against the previous one; yarn ui-checkstyle:changed clean.

UI screen recording:

N/A — backend-only change with a single provider hand-rolled parameter addition, no UI surface change.

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; no new blocking issue was established.

Summary

The PR requires credentials for push-feed connections and shares identity checks with HTTP authentication.

  • Push-feed sockets now use only an identity checked from a token or session.
  • The browser refreshes its socket token before reconnecting.

No new actionable issues were established. All supplied previous threads were unnumbered, so none receive numbered previous-finding entries.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Push-feed request] --> B{Transport}
  B -->|Polling| C[Servlet filter]
  B -->|WebSocket| D[Upgrade creator]
  C --> E[SocketAddressFilter.authenticate]
  D --> E
  E -->|Rejected| F[HTTP error]
  E -->|Accepted| G[Validated identity]
  G --> H[Stamp connection headers]
  H --> I[WebSocketManager]
Loading

Reviews (6) · Last reviewed commit: "Authenticate the push-feed identity on W..." · Reviewed by Greptile

…ions

Socket handshakes on the push-feed endpoint are now always authenticated:
a presented token is validated via the authorizer/authentication
configuration, a session cookie is validated against the session store,
and a handshake carrying neither is rejected. Previously, token
validation was gated on the authorizer's secure-socket flag, so under
the default configuration a handshake with no credential could bind to
any user id it presented.

The socket identity and session no longer fall back to raw query params
in the connection handler, and the
enableSecureSocketConnection authorizer setting is now ignored and
marked deprecated in the schema and sample configuration.
@TeddyCr
TeddyCr requested a review from a team as a code owner October 23, 2026 23:16
@TeddyCr
TeddyCr requested review from sonika-shah and a balanced review from Copilot October 23, 2026 23:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

❌ PR checklist incomplete

This PR cannot be merged until the following are addressed on its linked issue:

  • No GitHub issue is linked. Link an issue in the Development section of the PR (or add Fixes #12345 to the description). For a same-org cross-repo issue, add Fixes open-metadata/<repo>#123 to the description.

The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically.

Maintainers can bypass this check by adding the skip-pr-checks label.

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Generated Sources Need Update

The generated TypeScript types and/or dereferenced JSON schemas are out of sync with the JSON schema changes.

Since this is a pull request from a forked repository, they cannot be automatically committed.
Please generate and commit them manually:

cd openmetadata-ui/src/main/resources/ui
./json2ts-generate-all.sh -l true
yarn parse-schema
git add src/generated/ src/jsons/ public/jsons/
git commit -m "Update generated TypeScript types and dereferenced schemas"
git push

After pushing the changes, this check will pass automatically.

… rule

- Browsers cannot set headers on a WebSocket upgrade, so a bearer token may
  be presented on the push-feed handshake via a token query parameter;
  SocketAddressFilter validates it like a header token. Public-client SSO
  sessions (IdP token, no session cookie) need this to keep working.
- Externally-issued tokens carry no session claim and are accepted on
  token validation alone; internally-issued tokens revalidate against the
  session store.
- The handshake servlet no longer logs the query string.
- The UI passes its stored token on the socket.io handshake query.
- enableSecureSocketConnection is marked deprecated: true in the schema
  and the committed TS/dereferenced outputs are regenerated.
- ADR:2026-10-26-push-feed-handshakes-always-authenticate records the rule.
@TeddyCr
TeddyCr requested a review from a team as a code owner October 26, 2026 17:52
@TeddyCr
TeddyCr requested review from shah-harshit and a balanced review from Copilot October 26, 2026 17:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🚦 Removed from the merge queue — failed_checks (2026-10-27T17:00:50Z)

Blocked the queue: integration-tests-mysql-elasticsearch

  • Postgresql PR Playwright E2E Tests — playwright / playwright-ci (data-asset-rules-01), playwright / playwright-ci (chromium-11), playwright / playwright-ci (ingestion-01), playwright / playwright-ci (ingestion-02), playwright / playwright-ci (chromium-04), playwright / playwright-ci (chromium-10), playwright / playwright-ci (chromium-03), playwright / playwright-ci (chromium-13), playwright / playwright-ci (chromium-05), playwright / playwright-ci (chromium-15), playwright / playwright-ci (import-export-01), playwright / playwright-ci (chromium-06)
  • Integration Tests - MySQL + Elasticsearch — integration-tests-mysql-elasticsearch, Integration Test Lane (rdf)
  • Maven Collate Tests — maven-collate-ci

@TeddyCr
TeddyCr added this pull request to the merge queue Oct 27, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🚦 Removed from the merge queue — failed_checks (2026-10-27T21:12:35Z)

Blocked the queue: playwright-summary

  • Postgresql PR Playwright E2E Tests — playwright-summary, playwright / playwright-ci (chromium-15), playwright / playwright-ci (ingestion-02), playwright / playwright-ci (import-export-02), playwright / playwright-ci (chromium-06), playwright / playwright-ci (chromium-13), playwright / playwright-ci (chromium-11), playwright / playwright-ci (data-asset-rules-01), playwright / playwright-ci (ingestion-01), playwright / playwright-ci (chromium-10), playwright / playwright-ci (chromium-04), playwright / playwright-ci (chromium-03), playwright / playwright-ci (import-export-01), playwright / playwright-ci (chromium-05)
  • Maven Collate Tests — maven-collate-ci

Failed Playwright tests:

  • Features/AutoPilot.spec.ts › Agents created by AutoPilot should be deleted
  • Features/BulkImport.spec.ts › Database
  • Features/BulkImport.spec.ts › Database Schema
  • Features/BulkImport.spec.ts › Database service
  • Features/BulkImport.spec.ts › Keyboard Delete selection
  • Features/BulkImport.spec.ts › Range selection
  • Features/BulkImport.spec.ts › Table
  • Features/BulkImportWithDotInName.spec.ts › Bulk edit existing entity with dot in service name
  • Features/BulkImportWithDotInName.spec.ts › CSV with quoted FQN loads correctly in import grid
  • Features/BulkImportWithDotInName.spec.ts › Column with dot in name under service with dot
  • Features/BulkImportWithDotInName.spec.ts › Database service with dot in name - export and reimport
  • Features/BulkImportWithDotInName.spec.ts › Full import cycle with dot in service name
  • Features/BulkImportWithDotInName.spec.ts › Import at database level with dot in service name
  • Features/BulkImportWithDotInName.spec.ts › Import at schema level with dot in service name
  • Features/BulkImportWithDotInName.spec.ts › Service name with multiple dots

@TeddyCr
TeddyCr added this pull request to the merge queue Oct 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🚦 Removed from the merge queue — failed_checks (2026-10-27T23:15:34Z)

Blocked the queue: playwright-summary

  • Postgresql PR Playwright E2E Tests — playwright-summary, playwright / playwright-ci (chromium-15), playwright / playwright-ci (ingestion-01), playwright / playwright-ci (chromium-06), playwright / playwright-ci (ingestion-02), playwright / playwright-ci (import-export-02), playwright / playwright-ci (chromium-04), playwright / playwright-ci (data-asset-rules-01), playwright / playwright-ci (chromium-03), playwright / playwright-ci (import-export-01), playwright / playwright-ci (chromium-13), playwright / playwright-ci (chromium-05), playwright / playwright-ci (chromium-10), playwright / playwright-ci (chromium-11)
  • Maven Collate Tests — maven-collate-ci

Failed Playwright tests:

  • Features/AutoPilot.spec.ts › Agents created by AutoPilot should be deleted
  • Features/BulkImport.spec.ts › Database
  • Features/BulkImport.spec.ts › Database Schema
  • Features/BulkImport.spec.ts › Database service
  • Features/BulkImport.spec.ts › Keyboard Delete selection
  • Features/BulkImport.spec.ts › Range selection
  • Features/BulkImport.spec.ts › Table
  • Features/BulkImportWithDotInName.spec.ts › Bulk edit existing entity with dot in service name
  • Features/BulkImportWithDotInName.spec.ts › CSV with quoted FQN loads correctly in import grid
  • Features/BulkImportWithDotInName.spec.ts › Column with dot in name under service with dot
  • Features/BulkImportWithDotInName.spec.ts › Database service with dot in name - export and reimport
  • Features/BulkImportWithDotInName.spec.ts › Full import cycle with dot in service name
  • Features/BulkImportWithDotInName.spec.ts › Import at database level with dot in service name
  • Features/BulkImportWithDotInName.spec.ts › Import at schema level with dot in service name
  • Features/BulkImportWithDotInName.spec.ts › Service name with multiple dots

@github-actions

Copy link
Copy Markdown
Contributor

🚦 Removed from the merge queue — manual (2026-10-28T05:08:51Z)

No failing check on merge-queue commit 5d9f442 — invalidated by an entry ahead in the queue, or a required check timed out.

The Jetty (ee10) upgrade negotiation runs before the servlet filter chain,
so SocketAddressFilter's validation and header stamping only covered the
polling transport; an upgrade arrived with raw client headers and, after
the query fallback was removed, WebSocketManager could not key the socket
under any identity. Collate's ChatWebSocketIT saw no chat events and the
UI's jobs tray never refreshed, failing the merge queue (collate ITs and
the Playwright export/import async-job suites).

The upgrade creator now runs the same handshake validation
(SocketAddressFilter.authenticate): a rejected upgrade answers the HTTP
status the filter would send, and the accepted identity is stamped into
the socket's connection headers, mirroring the servlet dispatch.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

…dshakes

The upgrade handler copied every raw upgrade header into the engine.io
connection headers and only overwrote UserId/SessionId when the validated
identity carried a value, so a client-sent SessionId survived next to an
external IdP token (no session claim) and a client-sent UserId survived a
cookie session with no stored user id. The polling path had the same gap:
HeaderRequestWrapper only shadowed a header the filter had stamped. Both
paths now drop the client's identity headers (case-insensitively) before
stamping, and a handshake that yields no user id at all is rejected with
401 instead of being accepted and never registered by WebSocketManager.

The handler also logged the parsed query map at INFO and the upgrade URI
and parameter map at DEBUG; since the token now rides the query, every
browser connection wrote a bearer token into the server log. Those lines
are gone and a log-capture test guards them.

Also: a real-JwtFilter regression test with enableSecureSocketConnection
explicitly false (the mocked-validator suite stayed green when the old
gate was restored), a startup warning when no authorizer configuration
leaves the filter without a validator or session store, the stale
jwtFilter comment, and the ADR consequences (upgrade handler logging,
proxy/access-log redaction of the token parameter, no-authorizer
deployments).
validatePrefixedTokenRequest is shared with the MCP transport, so its
switch to JwtFilter.resolveUserName also changes which MCP bearer tokens
authenticate: with emailClaim configured, a token carrying only an email
claim now resolves to the stored username as it does over HTTP. Pin that
with a unit test on authenticateRequest.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@github-actions

Copy link
Copy Markdown
Contributor

🚦 Removed from the merge queue — failed_checks (2026-10-28T22:37:13Z)

Blocked the queue: integration-tests-postgres-opensearch

authenticateUpgrade answers a rejected handshake with 401/403 and
returns null, but the upgrade-creator lambda passed that null straight
into Jetty12WebSocketHandler, whose constructor rejects it with
Objects.requireNonNull. Every rejected upgrade (no credentials, bad
token, user mismatch) then surfaced as a NullPointerException out of
the WebSocket creator, which Jetty logs as an unexpected upgrade
failure that an anonymous client can trigger at will. A null creator
result is Jetty's supported way to stop the negotiation, so the lambda
now builds the handler only for an authenticated upgrade.
@TeddyCr

TeddyCr commented Oct 29, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed in 8b7782f. The upgrade creator now returns null when authenticateUpgrade yields no identity, so a rejected handshake — 401/403 already written — stops the negotiation instead of constructing the handler. Confirmed against Jetty 12.1's CreatorNegotiator.negotiate: a null creator result is the supported no-endpoint path, whereas the previous NPE escaped through Callback.failed and was logged as an unexpected upgrade failure on every anonymous probe. Full socket unit suite passes (61 tests, incl. the 22 rejection-path cases in SocketAddressFilterTest).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@github-actions

Copy link
Copy Markdown
Contributor

🚦 Removed from the merge queue — failed_checks (2026-10-29T02:28:29Z)

Blocked the queue: integration-tests-postgres-opensearch

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@gitar-bot

gitar-bot Bot commented Oct 29, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 6 closed / 6 findings

🔴 High risk · The new token/session gate and identity binding change authorization decisions; misvalidation could expose another user's push-feed data.

Adds credential validation to push-feed WebSocket handshakes under every configuration, accepting a bearer token from the header or token query parameter, or a session cookie, and binding the socket to the same identity HTTP requests resolve. Resolved the UI public-client SSO handshake rejection, the missing ADR for the handshake rule, the async token refresh timing, the client-sent UserId/SessionId header leak, and the NPE on rejected upgrades. Socket reconnects reusing an init-time token that expires is closed without a fix in this PR. No open issues remain.

✅ 6 closed
✅ Bug: UI sockets now rejected for public-client SSO (no cookie, no token sent)

📄 openmetadata-service/src/main/java/org/openmetadata/service/socket/SocketAddressFilter.java:196-203 📄 openmetadata-service/src/main/java/org/openmetadata/service/socket/WebSocketManager.java:85
The UI opens the socket with io({ query: { userId }, transports: ['websocket'] }) (WebSocketProvider.tsx:47-57). It never sends an Authorization header, and a browser WebSocket handshake can't carry one. So the only credential the UI ever presents is the OM_SESSION cookie. That cookie is only written by SessionService.createActiveSession/activatePendingSession, which run for the Basic, LDAP, SAML and confidential OIDC flows. Public-client OIDC is still supported and is the UI's default (authConfig?.clientType ?? ClientType.Public in AuthProvider.tsx:343). In that mode the browser holds the tokens and has no session cookie, so every handshake now goes into validateCookielessHandshake with tokenPrincipal == null and gets a 401. Activity feed, notifications and job-status channels stop working for those deployments, not only for the custom non-UI clients the description mentions. Possible fixes: have the UI pass its access token (for example via auth/query, validated with JwtFilter), or make sure public-client logins get a server session cookie. Also add a test or IT that covers a public-client handshake.

✅ Quality: Socket handshake auth rule and deprecated schema field have no ADR

📄 openmetadata-spec/src/main/resources/json/schema/configuration/authorizerConfiguration.json:108 📄 openmetadata-service/src/main/java/org/openmetadata/service/socket/SocketAddressFilter.java:76-89 📄 conf/openmetadata.yaml:481
This PR changes a schema contract: enableSecureSocketConnection in authorizerConfiguration.json is now a deprecated no-op. It also sets a cross-component rule that exists only in the PR description and code comments: "A presented bearer token is always validated ... A handshake carrying neither a token nor a session cookie is rejected ... The connection handler keys a socket only under the identity validated and stamped by the handshake filter, never from raw query parameters." Nothing in docs/decisions/ records this rule. Add a record that states: "Push-feed socket handshakes are authenticated under every configuration. The socket identity is taken only from the UserId header stamped by SocketAddressFilter, and enableSecureSocketConnection is kept only so existing YAML still loads."

✅ Bug: Async token refresh lands after reconnect attempt already sent

📄 openmetadata-ui/src/main/resources/ui/src/context/WebSocketProvider/WebSocketProvider.tsx:66-72
In socket.io-client 4.8, Manager.reconnect() emits reconnect_attempt and then calls open() right away in the same callback. open() creates the Engine, and the Engine copies opts.query into the transport (manager.ts, engine socket.ts). getOidcToken() returns a promise, so manager.opts.query.token = freshToken only runs after that copy. Every attempt goes out with the previous token. If the initial token has expired, attempt 1 still uses it, and each later attempt is one refresh behind. With reconnectionAttempts: 3, this can still use up the reconnect budget, which is the failure the added comment says this code prevents. Fix: write the token synchronously before open() runs. One way is to keep the latest token in a ref that is updated outside the event, and assign it inside the handler without awaiting. Another is to listen for reconnect_error/connect_error, refresh the token, and call manager.connect() / socket.connect() yourself after the promise resolves.

✅ Security: Client-sent UserId/SessionId upgrade headers survive when identity lacks one

📄 openmetadata-service/src/main/java/org/openmetadata/service/socket/Jetty12WebSocketHandler.java:113-127
onOpen copies every raw upgrade header into this.headers, and stampValidatedIdentity only overwrites UserId/SessionId when the validated identity has a value. WebSocketManager (lines 80-85) reads initialHeaders.get("UserId") and get("SessionId") straight from this map. So when the identity has a null field, whatever the client put in that header gets used. That happens for an external IdP token with no session claim (sessionId is null, and that path is explicitly accepted), and for a cookie session with no stored userId and no userId query param (userId is null). In those cases a header like SessionId: <any> or UserId: <victim uuid> keys or binds the socket from client input, which breaks the ADR rule that identity comes only from validated stamping. Fix: remove any client-supplied UserId/SessionId headers, matching names case-insensitively, before stamping. Only the validated values should ever reach the map.

✅ Bug: Rejected upgrades now throw NPE: creator wraps null identity in handler

📄 openmetadata-service/src/main/java/org/openmetadata/service/socket/Jetty12WebSocketHandler.java:57-60 📄 openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java:1435-1449
authenticateUpgrade deliberately returns null after it writes the 401/403 response. Its comment says "null only stops the socket negotiation". The upgrade-creator lambda still passes that result straight into new Jetty12WebSocketHandler(...), and this commit adds Objects.requireNonNull(handshakeIdentity, "handshakeIdentity") to the constructor. So every rejected push-feed upgrade (no credentials, bad token, user mismatch) now throws a NullPointerException out of the WebSocket creator, instead of returning no endpoint. Jetty then treats the rejection as an unexpected creator failure. This adds error-level stack traces for routine unauthenticated attempts, which an anonymous client can trigger at will, and may replace the intended status code. No test covers the creator's rejection path. Fix: have the lambda return null when the identity is null, so the handler is built only for an authenticated upgrade.

...and 1 more closed from earlier reviews

Verification

Rules No rules evaluated

Functional validation Not enabled

Set up

Auto-approval Not enabled

Set up

For more verification details, comment gitar display:verbose.

Options

Display: compact → Showing compact verification results.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source

@sonarqubecloud

Copy link
Copy Markdown

@sonarqubecloud

Copy link
Copy Markdown

This branch was successfully deployed

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

Labels

Ingestion 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.

4 participants