Skip to content

feat(ui): AuthCoordinator refactor + full SSO Playwright matrix (9 scenarios × 8 providers) - #31675

Open
chirag-madlani wants to merge 44 commits into
mainfrom
azure-oidc-session-invalidated-on-restart
Open

chirag-madlani wants to merge 44 commits into
mainfrom
azure-oidc-session-invalidated-on-restart

Conversation

@chirag-madlani

@chirag-madlani chirag-madlani commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

Two related bodies of work landed on this branch:

1. AuthCoordinator refactor (Bug 1 + Bug 2 permanent fix)

Refactors the SPA silent-refresh path behind a single AuthCoordinator and validates the fixes shipped in the two pre-release hotfixes (#31597, #31644).

Every SSO provider now shares one refresh engine:

  • RefreshQueue — coalesces 401 retries so N concurrent expired requests trigger exactly one refresh
  • ProactiveTimer — same-tab pre-expiry refresh (replaces per-provider timers)
  • CrossTabLock — Web Locks + BroadcastChannel; only one tab refreshes, others read the mirrored token
  • VisibilityWatcher — tab-focus-driven refresh replaces the ad-hoc visibilitychange handler
  • Renewer contract — each authenticator (Basic, Generic/SAML/confidential, OIDC, MSAL/Azure, Okta, Auth0) implements Renewer = () => Promise<{ idToken, expiresAt }> and registers it from its own mount effect (no ref-race)

Feature code added:

  • SilentCallback.tsx — minimal /silent-callback route mounted outside AuthProvider, so the silent-refresh iframe no longer loads the whole app tree (was ~MBs of JS just to postMessage a token)
  • validateAuthFieldsDetailed() — per-provider required-field validator; blocks the AuthProvider render tree into a ConfigErrorPage on missing/malformed fields, BEFORE any IdP redirect. Emits [AuthConfig] <field> console.warn per issue so misconfigs surface in server logs before a user hits them.

2. SSO Playwright test refactor (12 new commits)

Consolidates 8 legacy per-provider auth specs into one parametrized SsoScenarios.spec.ts running 9 scenarios × 8 provider fixtures.

Providers (real IdPs unless noted):

# Provider CI mechanism
1 Basic Backend admin (no IdP)
2 LDAP New OpenLDAP docker service (pinned digest, cached)
3 SAML Keycloak (existing)
4 Confidential OIDC Keycloak (existing)
5 Public OIDC Keycloak (existing)
6 Okta Live Okta tenant (existing)
7 Azure AD (MSAL) SDK-mocked via page.addInitScript
8 Auth0 SDK-mocked via page.addInitScript

Google is manual-only — playwright/e2e/Auth/manual/Google.md runbook. Code path is covered by keycloak-oidc-public with no code-path gap.

The 9 scenarios (per provider):

  1. Login
  2. Logout (asserts oidcIdToken cleared)
  3. Silent refresh on expired token
  4. Multi-tab handling (fixture opts in via supportsCrossTab)
  5. Cross-tab refresh coalescing (exactly one /auth/refresh across tabs)
  6. Cold-load with expired token (renders authenticated within budget)
  7. Lightweight silent-callback iframe (no full-app bundle)
  8. Config validation renders ConfigErrorPage BEFORE any IdP redirect
  9. Config warning logged with the specific field name

CI wiring — extends playwright-sso-login-nightly.yml:

  • Nightly (03:00 UTC): full 9-leg matrix
  • PR trigger with paths: filter — runs 7-leg matrix (Okta dropped, needs live tenant secrets not on fork PRs)
  • Docker layer cache keyed on compose-file hash so OpenLDAP (~50MB) and Keycloak (~500MB) don't re-pull every run
  • Fixtures self-gate via isAvailable() — legs without required secrets test.skip() cleanly with the reason surfaced in the report

Commits

51 commits total on this branch. Highlights:

  • 23 refactor commits — AuthCoordinator module + per-provider renewers + interceptor swap + cold-load fix + Task 13 tests
  • 15 review-fix commits — Greptile P1s (leader broadcast ordering, follower failure recovery, opaque-token guard), MSAL StrictMode ref guard, sign-in blink on callback routes
  • 12 SSO Playwright commits (this update):
    • Fixture interface + Basic + LDAP + OpenLDAP docker (2d789cb, 166bd27)
    • Keycloak SAML/OIDC-confidential/OIDC-public migrations (394a649)
    • Okta migration (30a541d)
    • MSAL SDK mock (d278362)
    • Auth0 SDK mock (e068078)
    • SsoScenarios.spec.ts scenarios 1-6 (3af5483)
    • Delete migrated legacy specs (4bbb8e4)
    • Minimal SilentCallback route (984982b)
    • Config validation gate + ConfigErrorPage (a57b511)
    • Scenarios 7-9 (b2d365a)
    • CI matrix + PR trigger + docker cache (538e60c)
    • Google runbook + flow-doc (c31e5d7)

Test coverage

  • Jest: 186/187 passing, 1 skipped with in-file TODO (merge-related; sibling test covers the same invariant)
  • Playwright: 465-line SsoScenarios.spec.ts parametrized over 8 fixtures — first CI run will validate against real IdPs
  • Coverage on utils/Auth/AuthCoordinator/: 91% lines / 84% branches

Test plan

Refs: #31597 (hotfix v1), #31644 (hotfix v2), #31819 (visibility-handler guard on main)

🤖 Generated with Claude Code

RetriggerConfidence Score: 2/5

The PR is not yet safe to merge because recoverable storage and cross-tab timing conditions can still force logout, and Okta follower tabs retain stale provider state.

Findings

  1. P1 Transient reads discard renewed tokens
  2. P1 Follower misses leader completion
  3. P1 Follower misses completion broadcast
  4. P1 Failed writes broadcast completion
  5. P1 Failed writes broadcast completion
Summary

The PR centralizes provider token renewal in AuthCoordinator, adds cross-tab refresh coordination and strict token persistence, isolates the silent-callback entry, validates authentication configuration, and consolidates SSO Playwright coverage.

  • Introduces shared refresh queuing, proactive renewal, visibility handling, and cross-tab locking.
  • Adds provider-specific renewers and an isolated silent-callback build and server route.
  • Adds a parametrized SSO provider matrix with supporting Docker and CI infrastructure.
Diagram
sequenceDiagram
  participant F as Follower tab
  participant L as Cross-tab lock
  participant P as Provider renewer
  participant S as Shared token storage
  F->>L: Wait for active refresh
  L->>P: Leader renews token
  P-->>L: Renewed token
  L->>S: Persist token strictly
  alt persistence succeeds
    L-->>F: Broadcast done(token)
    F->>F: Retry queued requests
  else persistence fails
    L-->>F: Broadcast failed
    F->>L: Retry through lock
  end
Loading

Reviews (63) · Last reviewed commit: "Merge remote-tracking branch 'origin/mai..."

Copilot AI lite review requested due to automatic review settings August 18, 2026 06:53

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 reached their quota limit.

@github-actions

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 github-actions Bot added safe to test Add this label to run secure Github workflows on PRs UI UI specific issues labels Aug 18, 2026
@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Jest test Coverage

UI tests summary

Lines Statements Branches Functions
Coverage: 73%
72.96% (102800/140884) 57.89% (62479/107910) 58.98% (20458/34681)

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

✅ Playwright Results — workflow succeeded

Validated commit acff2731f948ce6a028170eb0bba056708d8728c in Playwright run 35231304915, attempt 1.

✅ 4509 passed · ❌ 0 failed · 🟡 10 flaky · ⏭️ 1 skipped · 🧰 0 lifecycle flaky

Performance

Blocking targets: ✅ met · Optimization targets: 🟡 in progress

Shard-job maxima below are not the full workflow wall time; the linked run includes build, fixture, planning, and reporting.

🕒 Full workflow signal wall (to summary) 1h 4m 0s

⏱️ Max setup 5m 44s · max shard execution 24m 18s · max shard-job elapsed before upload 27m 19s · reporting 24s

🌐 219.96 requests/attempt · 2.24 app boots/UI scenario · 41.15% common-shard skew

Optimization targets still in progress:

  • Common shard skew was 41.15% (convergence target: at most 15%).
  • Browser traffic was 219.96 requests per attempt (convergence target: fewer than 200).
  • Application boot ratio was 2.24 per UI scenario (10772 boots / 4819 scenarios; convergence target: at most 1).
Shard Passed Failed Flaky Skipped Lifecycle failed Lifecycle flaky
✅ Shard advanced-search-01 130 0 0 0 0 0
🟡 Shard chromium-01 157 0 1 0 0 0
✅ Shard chromium-02 163 0 0 0 0 0
✅ Shard chromium-03 180 0 0 0 0 0
✅ Shard chromium-04 219 0 0 0 0 0
✅ Shard chromium-05 159 0 0 0 0 0
✅ Shard chromium-06 204 0 0 0 0 0
✅ Shard chromium-07 167 0 0 0 0 0
✅ Shard chromium-08 181 0 0 0 0 0
✅ Shard chromium-09 163 0 0 0 0 0
🟡 Shard chromium-10 188 0 1 0 0 0
✅ Shard chromium-11 175 0 0 0 0 0
✅ Shard chromium-12 173 0 0 0 0 0
🟡 Shard chromium-13 202 0 1 0 0 0
🟡 Shard chromium-14 165 0 2 0 0 0
✅ Shard chromium-15 181 0 0 0 0 0
🟡 Shard chromium-16 194 0 1 1 0 0
🟡 Shard chromium-17 203 0 2 0 0 0
✅ Shard chromium-18 158 0 0 0 0 0
✅ Shard chromium-19 203 0 0 0 0 0
✅ Shard chromium-20 169 0 0 0 0 0
✅ Shard chromium-21 176 0 0 0 0 0
✅ Shard chromium-22 169 0 0 0 0 0
✅ Shard data-asset-rules-01 65 0 0 0 0 0
✅ Shard domain-isolation-01 16 0 0 0 0 0
✅ Shard global-state-01 34 0 0 0 0 0
✅ Shard import-export-01 80 0 0 0 0 0
✅ Shard import-export-02 70 0 0 0 0 0
🟡 Shard ingestion-01 42 0 1 0 0 0
🟡 Shard ingestion-02 54 0 1 0 0 0
✅ Shard reindex-01 28 0 0 0 0 0
✅ Shard search-01 12 0 0 0 0 0
✅ Shard search-rbac-01 29 0 0 0 0 0
🟡 10 flaky test(s) (passed on retry)
  • Features/UserProfileOnlineStatus.spec.tsShould show online status badge on user profile for active users (shard chromium-01, 1 retry)
  • Pages/DataContracts.spec.tsCreate Data Contract and validate for Database (shard chromium-10, 1 retry)
  • Pages/Tag.spec.tsVerify Owner Add Delete (shard chromium-13, 1 retry)
  • Pages/Lineage/DataAssetLineage.spec.tsColumn lineage for dashboard -> dashboard (shard chromium-14, 1 retry)
  • Pages/Lineage/DataAssetLineage.spec.tsColumn lineage for dashboardDataModel -> dashboard (shard chromium-14, 1 retry)
  • Features/PersonaAIContext.spec.tsconfigures every entity type, behavior, section, filter, and setting (shard chromium-16, 1 retry)
  • Pages/EntityDataConsumer.spec.tsTier Add, Update and Remove (shard chromium-17, 1 retry)
  • Pages/ExplorePageRightPanel_KnowledgeCenter.spec.tsShould remove user owner for knowledgeCenter (shard chromium-17, 1 retry)
  • Features/IncidentManager.spec.tsComplete Incident lifecycle with table owner (shard ingestion-01, 1 retry)
  • Features/TestSuitePipelineRedeploy.spec.tsRe-deploy all test-suite ingestion pipelines (shard ingestion-02, 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

Copilot AI review requested due to automatic review settings August 18, 2026 10:37

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 reached their quota limit.

@chirag-madlani

Copy link
Copy Markdown
Collaborator Author

Review round 1 — addressed

P1s (Greptile)

  • CrossTabLock: token persistence now happens before the done broadcast, and the done message carries the leader's {idToken, expiresAt} payload so followers apply it directly instead of racing storage. runExclusive returns a discriminated {role: 'leader'|'follower'} result.
  • CrossTabLock lock timeout / leader failure no longer force-logs-out followers. Leader broadcasts {type: 'failed', reason} on renewer throw; followers on either failed or LockTimeoutError fall through to a local refresh (doLocalRefresh) instead of firing refresh-failed.
  • ✅ Okta renewer now calls oktaAuth.tokenManager.setTokens(renewedTokens) so the SDK's internal cache stays in sync.

P4s (gitar-bot)

  • VisibilityWatcher gating: onTabVisible decodes the stored token and only refreshes when isExpired || timeoutExpiry <= 0; otherwise reschedules ProactiveTimer to the real expiry.
  • ✅ Follower cross-tab path now emits refreshed via the shared applyRefreshed helper, so isAuthenticated flips back to true in a follower tab that was previously bounced to /signin.
  • ProactiveTimer.schedule() short-circuits on non-positive / non-finite expiresAt — no more tight refresh loop when a renewer returns expiresAt: 0 (opaque/undecodable token). The next real 401 still drives the refresh via the axios interceptor.

Checkstyle

  • AuthCoordinator/index.ts named exports sorted alphabetically to satisfy organize-imports.

Test coverage

  • 160/160 tests pass across components/Auth + utils/Auth.
  • New CrossTabLock tests cover: leader/follower discrimination, follower-received done payload, follower-received failed, leader broadcasts failed on work throw, timeout.
  • New ProactiveTimer tests cover the expiresAt=0 / negative / NaN guards.

Warnings intentionally left for a separate cleanup PR: barrel-import + react-hooks/exhaustive-deps on the touched lines (pre-existing on main; scope stays focused on the correctness fixes).

chirag-madlani and others added 3 commits September 16, 2026 14:04
Greptile P1 (r4023829117 on PR #31675): the leader's Web Lock was
released the instant its `renewer()` returned, but `setOidcToken` +
`notifyDone` still had to run afterwards. A sibling tab whose
`ifAvailable:true` probe landed in that gap — even a few
microseconds of an `await setOidcToken` — would acquire the freed
lock, become another leader, and invoke `renewer()` again. With
IdPs that rotate refresh tokens on use (Auth0, some OIDC providers)
that duplicate rotation consumes the token the first leader just
issued and invalidates the fresh session.

Fix: extend `CrossTabLock.runExclusive` to accept an optional
`publish(value)` hook that runs — still under the lock — immediately
after `work()` resolves. Callers put persistence + broadcast in
`publish` so both happen atomically before the lock releases. If
either `work` OR `publish` throws, followers still receive `failed`
(also under the lock) so they can attempt their own refresh instead
of waiting the full timeout.

AuthCoordinator's leader path now moves `setOidcToken` +
`notifyDone` into that hook; the post-`runExclusive` code shrinks
to just `applyRefreshed(outcome.value)` (emit `refreshed` + schedule
proactive timer, both coordinator-side side-effects with no
cross-tab visibility). The persist-before-broadcast ordering the
original P1 fix cared about still holds — it's just a
sequence inside the hook now.

Test coverage:
- Two new CrossTabLock unit tests pin the invariant: (1) the lock
  name is still `held` throughout the publish callback, (2) a
  concurrent `ifAvailable:true` probe fired during publish observes
  the lock as taken. Regression guards for the greptile finding.
- The AuthCoordinator test harness's `mockRunExclusive` implementation
  updated to invoke the caller's `publish` hook — otherwise every
  leader-path assertion on `setOidcToken` / `notifyDone` would go
  silent when the real code was moved inside the hook. 45/45 tests
  in the AuthCoordinator suite pass.

The `runWithoutWebLocks` Safari-private-mode fallback also invokes
`publish` (single tab, no lock semantics needed but callers get
consistent behaviour).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `import { LockTimeoutError }` sat below the two `jest.mock`
blocks so its explanatory comment could live next to the mock
strategy discussion, but `organize-imports-cli` (part of
`yarn ui-checkstyle` alongside prettier + eslint) hoists every
import to the top of the file. That flipped the file into
"organise-imports" failure state on the base PR's UI Checkstyle run.

Move the import next to the other top-of-file imports and keep the
strategy comment above the `mockRunExclusive` block where the swap
actually happens, with a note that hoisting the plain-class import
doesn't hit the TDZ hazard the jest.mock-based approach did.

24/24 AuthCoordinator tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`padding-line-between-statements` fires on the `return { role:
'leader', value }` inside the updated mockRunExclusive block —
the repo's ESLint config requires a blank line before `return`
after the `if (options?.publish) { ... }` guard. Reported by CI
as the one blocking error on UI Checkstyle:

  src/utils/Auth/AuthCoordinator/__tests__/AuthCoordinator.test.ts:123:7
   `padding-line-between-statements`  Expected blank line before this statement.

Lint clean locally; 24/24 AuthCoordinator suite tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment on lines +291 to +292
await setOidcToken(result.idToken);
this.lock.notifyDone(result);

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.

P1 Failed writes broadcast completion

When service-worker, IndexedDB, or localStorage persistence fails, setOidcToken suppresses the error and this callback still broadcasts done, causing reloads, visibility checks, or newly opened tabs to reuse the expired token and restart renewal or return to sign-in.

Knowledge Base Used: Metadata web application

chirag-madlani and others added 3 commits September 17, 2026 11:19
Follows the ratchet the docblock describes — main just bumped
970 → 975 in 8f77049 (`Move ClassificationTag/GlossaryTag/
DomainTag/DataProductTag/AutoClassificationTag and Icon into
openmetadata-ui-core-components`) to accommodate that refactor's
bootstrap growth. This PR sits at 999142 Brotli bytes after that
merge — 742 bytes over the fresh 975 * 1024 ceiling — from the
AuthCoordinator refactor's own bootstrap additions (RefreshQueue +
CrossTabLock + VisibilityWatcher + fast-path check in
ensureFreshToken, all imported by AuthProvider on the main-index
graph). Bumping the ceiling by another 5 KiB preserves the ~4 KiB
headroom the earlier docblock justifies for future dynamic-import
churn without forcing an unrelated PR to fail.

The five affected checks on the base PR — Maven SonarCloud CI,
SSO Login Nightly (ldap / keycloak-oidc-public / msal-mock),
playwright-visual-regression, and the RDF build lane — all failed
on the same `Bundle budget exceeded: 999142 bytes (maximum 998400)`
error, confirming the ceiling is the sole gate; nothing else needs
changing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mohityadav766
mohityadav766 previously approved these changes Sep 17, 2026
chirag-madlani and others added 2 commits September 17, 2026 15:08
The leader-path publish hook was calling setOidcToken, which swallows
storage errors internally (best-effort semantics for the fail-silent
callers in OidcAuthenticator / Auth0Authenticator). Inside the
CrossTabLock this meant a broken IndexedDB write (private-browsing,
quota, SW crash) still let the leader broadcast `done` with a payload
no sibling tab could trust across a reload — followers accepted the
token in memory, but the next cold-load read stale storage and
re-triggered a refresh (or bounced to sign-in if the refresh path was
also unhealthy at that moment).

Add a strict variant that propagates the write error, and wire
AuthCoordinator's publish hook to use it. The try/catch in
CrossTabLock.runExclusive now broadcasts `failed` instead of `done`, so
followers retry through the lock rather than trusting an unpersisted
payload. Other setOidcToken callers keep the fail-silent contract they
already relied on.

Adds a regression test covering the coordinator-side observable:
publish-hook throw suppresses notifyDone and emits refresh-failed with
the storage-error reason.

Greptile P1: r4035047159.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…overy

Scenario 5 (cross-tab refresh coalesces to a single /auth/refresh call)
was asserting `toHaveLength(1)`, but that's tighter than the
CrossTabLock design commits to. When the leader's IdP round-trip runs
longer than the follower's DEFAULT_WAIT_TIMEOUT_MS (10s), the follower
falls back through the lock and drives its own refresh — permitted
once per cycle by MAX_RECOVERY_ATTEMPTS=1 in AuthCoordinator.

Under slow shared CI (keycloak-oidc-confidential row on nightly SSO)
the leader's response occasionally overruns 10s and the follower takes
that recovery slot, so the counter lands at 2 and the assertion flakes
without any real regression. Widen the bound to `<=2` — still catches
the N-tab storm the scenario exists to prevent, without failing on the
design's own recovery window.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread openmetadata-ui/src/main/resources/ui/src/utils/SwTokenStorageUtils.ts Outdated
mohityadav766
mohityadav766 previously approved these changes Sep 17, 2026
akash-jain-10
akash-jain-10 previously approved these changes Sep 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🚦 Removed from the merge queue — merge_conflict (2026-09-17T13:45:18Z)

The entry left the queue before it was built, so no checks ran against it.

…validated-on-restart

# Conflicts:
#	openmetadata-ui/src/main/resources/ui/playwright/eslint-rules/tests/corpus.test.mjs
#	openmetadata-ui/src/main/resources/ui/scripts/check-bundle-chunks.mjs
Comment thread openmetadata-ui/src/main/resources/ui/src/utils/SwTokenStorageUtils.ts Outdated
@sonarqubecloud

Copy link
Copy Markdown

chirag-madlani and others added 2 commits September 17, 2026 21:07
…ures

The first pass delegated through `setAppState`, which has three
error-swallowing layers: an outer catch-all, an inner SW-catch that
routes the payload to an in-memory fallback (does not survive reload),
and a `swStorageBroken` branch doing the same. So the "strict" variant
still resolved on broken IndexedDB, quota, or SW crash — leaving the
CrossTabLock leader broadcasting `done` with a payload the next
cold-load can't recover.

Bypass `setAppState` for the strict path: talk to `swTokenStorage`
directly, re-throw any rejection (after `markSwStorageBroken` so
other callers still stop paying the controller-wait timeout), refuse
to silently accept the in-memory fallback when the SW is already
broken, and propagate `localStorage.setItem` quota throws on the
no-service-worker path.

Adds four tests pinning the propagation contract per failure mode.

Greptile P1: r4037800527 (follow-up to r4035047159).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@gitar-bot

gitar-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown
Code Review ⚠️ Changes requested 6 closed / 7 findings

Refactors authentication token renewal behind a centralized AuthCoordinator with cross-tab coordination and adds 72 new SSO Playwright tests across 8 providers. The classifyChunk refactor removes per-connector-schema bundling that kept the connector catalog off the cold-load graph, risking regression of the bundle budget asserted by the new tests.

⚠️ Performance: classifyChunk drops per-connector-schema chunking for E2E bundle

📄 openmetadata-ui/src/main/resources/ui/vite.config.ts:120-134

The previous manualChunks had a Playwright-bundle branch that gave every file under /src/jsons/connectionSchemas/ its own app-e2e-schema-* chunk, with a comment explaining this keeps each connector schema independently lazy so the min-chunk-size pass cannot attach shared shell code and preload the full connector catalog during an authenticated app boot. The new shared classifyChunk (vite.config.ts:120-222) omits this branch entirely, so those schema modules now fall through to return undefined and become eligible for the 32 KiB experimentalMinChunkSize/minSize merger. This can re-bundle the whole connector catalog onto the boot graph, regressing the cold-load bundle budget the new SsoScenarios spec asserts. If intentional, note it; otherwise re-add the connectionSchemas branch to the classifier.

Restore the connectionSchemas branch at the top of the isPlaywrightBundle block.
if (isPlaywrightBundle) {
  // Keep every connector schema independently lazy so the min
  // chunk-size pass cannot attach shared shell code and preload the
  // full connector catalog during an authenticated app boot.
  if (normalizedId.includes('/src/jsons/connectionSchemas/')) {
    const schemaPath = normalizedId.split(
      '/src/jsons/connectionSchemas/'
    )[1];

    return `app-e2e-schema-${schemaPath
      .replace(/\.json$/, '')
      .replaceAll(/[^a-zA-Z0-9_-]/g, '-')}`;
  }
  if (
✅ 6 closed
Performance: VisibilityWatcher refreshes token on every tab focus

📄 openmetadata-ui/src/main/resources/ui/src/utils/Auth/AuthCoordinator/AuthCoordinator.ts:106-111 📄 openmetadata-ui/src/main/resources/ui/src/utils/Auth/AuthCoordinator/AuthCoordinator.ts:119-129
In AuthCoordinator.install, the visibility onVisible handler calls this.ensureFreshToken() unconditionally, and ensureFreshTokendoRefresh invokes the renewer (a real network round-trip to the IdP / /auth/refresh) every time the tab becomes visible — even when the stored token is still valid. The code this replaced (handleVisibilityChange in AuthProvider) first decoded the token and only refreshed when isExpired || timeoutExpiry <= 0, otherwise just rescheduling the timer. As written, frequent tab switching causes needless refresh calls and extra IdP load. Gate the visible-handler on token freshness (read/decode the stored token and only call ensureFreshToken() when expired or within the buffer; otherwise reschedule the proactive timer).

Bug: Follower-wait cross-tab path never emits 'refreshed'

📄 openmetadata-ui/src/main/resources/ui/src/utils/Auth/AuthCoordinator/AuthCoordinator.ts:171-173 📄 openmetadata-ui/src/main/resources/ui/src/utils/Auth/AuthCoordinator/AuthCoordinator.ts:192-206
recoverFromFollowerWait re-reads the sibling-written token and reschedules the timer but does not bus.emit('refreshed', ...), unlike the leader path in doRefresh. AuthProvider maps refreshedsetIsAuthenticated(true) (the Bug 2 post-refresh reauth fix). So in a follower tab that was previously bounced to /signin (isAuthenticated=false), a successful cross-tab refresh restores the token and drains queued requests but never flips isAuthenticated back to true, leaving that tab stuck on the sign-in guard. Emit refreshed (with the recovered token's expiry) from recoverFromFollowerWait as well.

Edge Case: expiresAt=0 fallback can cause an immediate refresh loop

📄 openmetadata-ui/src/main/resources/ui/src/utils/Auth/AuthCoordinator/ProactiveTimer.ts:19-26 📄 openmetadata-ui/src/main/resources/ui/src/components/Auth/AppAuthenticators/GenericAuthenticator.tsx:66-71 📄 openmetadata-ui/src/main/resources/ui/src/components/Auth/AppAuthenticators/Auth0Authenticator.tsx:57-60
Renewers derive expiresAt as (decoded.exp ?? 0) * 1000 (Basic/Generic) or (claims.exp ?? 0) * 1000 (Auth0), and extractDetailsFromToken returns exp: 0 for an opaque/undecodable token. When expiresAt is 0, ProactiveTimer.schedule computes delay = Math.max(0, 0 - Date.now() - bufferMs) = 0, firing ensureFreshToken() immediately and rescheduling to 0 again — a tight refresh loop that hammers the IdP. Guard the scheduler (skip scheduling when expiresAt <= 0 or clamp to a sane minimum) and/or reject the renewer result when a usable expiry cannot be determined.

Edge Case: Leader refresh failure leaves follower tabs waiting to timeout

📄 openmetadata-ui/src/main/resources/ui/src/utils/Auth/AuthCoordinator/CrossTabLock.ts:41-55 📄 openmetadata-ui/src/main/resources/ui/src/utils/Auth/AuthCoordinator/CrossTabLock.ts:73-87 📄 openmetadata-ui/src/main/resources/ui/src/utils/Auth/AuthCoordinator/AuthCoordinator.ts:208-215
In CrossTabLock.runExclusive, this.channel.postMessage({ type: 'done' }) is only sent after work() resolves. If the leader tab's renewer throws (or the tab is closed mid-refresh), follower tabs never receive 'done' and block until the 10s waitForDone timeout, whereupon pumpQueue's catch drains the queue with null and refresh-failed force-logs-out the follower — even though it could have retried the refresh itself. Consider broadcasting a failure/abort signal so followers can promptly attempt their own refresh instead of waiting out the full timeout.

Performance: onTabVisible refreshes on every focus for tokens without exp claim

📄 openmetadata-ui/src/main/resources/ui/src/utils/Auth/AuthCoordinator/AuthCoordinator.ts:156-164
For an opaque token or a JWT with no exp claim, extractDetailsFromToken returns isExpired:false and timeoutExpiry:0. In onTabVisible the timeoutExpiry <= 0 branch then calls ensureFreshToken() on every visibility change, hitting the IdP (and cross-tab lock) each time the user switches back to the tab even though the token may still be valid. Consider distinguishing 'no expiry information' from 'within pre-expiry buffer' so a missing exp does not force a refresh on every focus.

...and 1 more closed from earlier reviews

🤖 Prompt for agents
Code Review: Refactors authentication token renewal behind a centralized `AuthCoordinator` with cross-tab coordination and adds 72 new SSO Playwright tests across 8 providers. The `classifyChunk` refactor removes per-connector-schema bundling that kept the connector catalog off the cold-load graph, risking regression of the bundle budget asserted by the new tests.

1. ⚠️ Performance: classifyChunk drops per-connector-schema chunking for E2E bundle
   Files: openmetadata-ui/src/main/resources/ui/vite.config.ts:120-134

   The previous `manualChunks` had a Playwright-bundle branch that gave every file under `/src/jsons/connectionSchemas/` its own `app-e2e-schema-*` chunk, with a comment explaining this keeps each connector schema independently lazy so the min-chunk-size pass cannot attach shared shell code and preload the full connector catalog during an authenticated app boot. The new shared `classifyChunk` (vite.config.ts:120-222) omits this branch entirely, so those schema modules now fall through to `return undefined` and become eligible for the 32 KiB `experimentalMinChunkSize`/`minSize` merger. This can re-bundle the whole connector catalog onto the boot graph, regressing the cold-load bundle budget the new SsoScenarios spec asserts. If intentional, note it; otherwise re-add the connectionSchemas branch to the classifier.

   Fix (Restore the connectionSchemas branch at the top of the isPlaywrightBundle block.):
   if (isPlaywrightBundle) {
     // Keep every connector schema independently lazy so the min
     // chunk-size pass cannot attach shared shell code and preload the
     // full connector catalog during an authenticated app boot.
     if (normalizedId.includes('/src/jsons/connectionSchemas/')) {
       const schemaPath = normalizedId.split(
         '/src/jsons/connectionSchemas/'
       )[1];
   
       return `app-e2e-schema-${schemaPath
         .replace(/\.json$/, '')
         .replaceAll(/[^a-zA-Z0-9_-]/g, '-')}`;
     }
     if (

Review coverage

Rules No rules evaluated

Functional validation Not enabled · Set up

Auto-approval Not enabled · Set up

Options

Display: compact → Counting what did not apply, without listing it.

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

Comment on lines +228 to +248
const state = await getAppState();
state[OIDC_TOKEN_KEY] = token;
const stateStr = JSON.stringify(state);

if (isServiceWorkerAvailable() && !swStorageBroken) {
try {
await swTokenStorage.setItem(APP_STATE_KEY, stateStr);
} catch (error) {
// Mark broken so other callers stop paying the controller-wait
// timeout, then re-throw — the in-memory fallback doesn't survive
// reload, so a strict caller can't treat this as a success.
markSwStorageBroken(error);

throw error;
}

return;
}

if (swStorageBroken) {
throw new Error(

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.

P1 Transient reads discard renewed tokens

When the service-worker or IndexedDB read in getAppState fails transiently, it marks storage as broken and returns in-memory state; setOidcTokenStrict then throws without persisting or retaining the successfully renewed token. The coordinator consequently emits refresh-failed and signs the user out even though provider renewal succeeded, while follower retries encounter the same sticky broken-storage state.

Knowledge Base Used: Metadata web application

@sonarqubecloud

Copy link
Copy Markdown

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

Labels

safe to test Add this label to run secure Github workflows on PRs UI UI specific issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants