apache / apache/superset

Machine auth's /login/ priming page can clobber the injected session cookie, sending screenshots to the login screen

Closed Beginner friendly
#44,005 1 comment 0 reactions 0 assignees View on GitHub
#bug
Dominant language
Python
Stars
74.8k
Forks
18.3k
Avg merge
2d 5h
Merged PRs (30d)
685

Description

### Bug description

`MachineAuthProvider.authenticate_browser_context()` opens a page on `/login/` to prime the
cookie jar, injects the authenticated session cookie into the **shared** browser context, and
then **never closes that page**. The login SPA keeps issuing requests after `page.goto()`
resolves. Any of those responses that carries `Set-Cookie: session=` and lands
*after* `add_cookies()` overwrites the injected cookie in the shared jar, and the entire
browser context silently reverts to anonymous.

From then on every request 302s to `/login/`, the screenshot page renders the login screen
instead of the dashboard, `.standalone` never appears, and the report fails with a misleading
timeout:

```
ERROR/ForkPoolWorker-3 Timed out requesting url https:///superset/dashboard//?force=false&standalone=3
ERROR/ForkPoolWorker-3 Screenshot failed after 129.80s - execution_id:
superset.commands.report.exceptions.ReportScheduleScreenshotFailedError: Failed taking a screenshot
Locator.wait_for: Timeout 120000ms exceeded.
Call log:
- waiting for locator(".standalone") to be visible
```

The error points at rendering, but nothing is slow — the browser is parked on a login page.
Raising `SCREENSHOT_PLAYWRIGHT_DEFAULT_TIMEOUT` does not help; it only makes the failure take
longer.

#### The code

`superset/utils/machine_auth.py:80-110` (6.1.0):

```python
def authenticate_browser_context(self, browser_context, user):
...
url = urlparse(app.config["WEBDRIVER_BASEURL"])

# Setting cookies requires doing a request first
page = browser_context.new_page() # (1) never closed
page.goto(headless_url("/login/")) # (2) resolves on 'load'; the SPA keeps fetching
cookies = self.get_cookies(user)
browser_context.clear_cookies()
browser_context.add_cookies([...]) # (3) shared jar — (1) can still clobber this
return browser_context
```

The comment on line 91 — *"Setting cookies requires doing a request first"* — is a Selenium-era
carryover. Selenium's `driver.add_cookie()` does require already being on the domain, which is
why `authenticate_webdriver()` navigates first. Playwright's `BrowserContext.add_cookies()`
takes `domain`/`path` (or `url`) directly and needs **no** prior navigation, so the goto buys
nothing on this path while creating a live page that outlives the injection.

#### Because it is a race, it is intermittent

It only loses when the login page is still settling as `add_cookies()` runs — i.e. when
`/login/` is slow. In our deployment that is reliably the first request after several hours of
idle: a weekly report scheduled at 05:00 UTC failed on two consecutive Mondays, while the same
report against the same dashboard, re-run by hand mid-day on a warm instance, succeeded every
time.

### Screenshots/recordings

Access log for the *same report and dashboard*, failing run vs. successful run. The
discriminator is the **priming page's own** `POST /superset/log/` (referer `…/login/`), which
fires after cookie injection:

Failing run (cold instance — `/login/` took ~4s to settle):

```
05:00:01 "GET /login/" 200 "-"
05:00:03 "GET /static/service-worker.js" 404 ".../login/"
05:00:05 "GET /superset/dashboard//?force=false&standalone=3" 302 "-"
05:00:05 "POST /superset/log/?explode=events" 302 ".../login/" <-- priming page is anonymous
05:00:06 "GET /login/?next=.../superset/dashboard//..." 200 "-"
05:00:10 "POST /superset/log/?explode=events" 302 ".../login/?next=..."
```

Successful run (warm instance — `/login/` settled in ~2s):

```
14:35:11 "GET /login/" 200 "-"
14:35:13 "GET /static/service-worker.js" 404 ".../login/"
14:35:13 "GET /superset/dashboard//?force=false&standalone=3" 200 "-"
14:35:14 "POST /superset/log/?explode=events" 200 ".../login/" <-- priming page is authenticated
14:35:17 "POST /api/v1/chart/data?...&dashboard_id=" 200 ".../superset/dashboard//..."
```

The priming page and the screenshot page are both anonymous in the failing run, so this is not
specific to the dashboard route, to RBAC, or to the report configuration — the whole context
lost its cookie.

Elapsed time confirms the auth navigation itself succeeded: total run was **129.80s** against a
`SCREENSHOT_PLAYWRIGHT_DEFAULT_TIMEOUT` of 120000ms — exactly one timeout plus ~10s of page
loads. A failing `page.goto()` on `/login/` would have consumed its own 120s and raised
`Page.goto: Timeout …` instead.

### Superset version

6.1.0

### Python version

3.11

### Node version

I don't know

### Browser

Chrome

### Additional context

#### Ruled out

- **Executor user.** `ALERT_REPORTS_EXECUTORS` is the default `[ExecutorType.OWNER]`; the
resolved owner is active and unchanged between failing and succeeding runs, so
`login_user()` succeeds and `get_auth_cookies()` returns a populated dict.
- **Session storage.** `SESSION_SERVER_SIDE` is the default `False` — plain signed cookies, no
shared server-side store to go stale.
- **Restarts / deploys.** gunicorn and the celery worker ran continuously across both failures.
- **Dashboard rendering.** The same dashboard screenshots in 30–36s when the context is
authenticated.

#### Suggested fix

Drop the priming navigation from the Playwright path:

```diff
- # Setting cookies requires doing a request first
- page = browser_context.new_page()
- page.goto(headless_url("/login/"))
-
cookies = self.get_cookies(user)

browser_context.clear_cookies()
browser_context.add_cookies([...])
```

If the navigation must stay for other reasons, `page.close()` before `clear_cookies()` would
also close the window.

We are running the equivalent as a `WEBDRIVER_AUTH_FUNC` override and it removes the failure,
while also saving a page load per screenshot.

#### Related

- **#34076** (closed) — same `page.goto(headless_url("/login/"))` line, different symptom: the
navigation itself times out waiting for `load`. Its accepted workaround is also overriding
`WEBDRIVER_AUTH_FUNC`.
- **#43253** (open) — the fix for #34076; passes `SCREENSHOT_PLAYWRIGHT_WAIT_EVENT` through to
that goto. Worth coordinating: it resolves the navigation **earlier**
(`domcontentloaded` rather than `load`), which leaves *more* login-page traffic in flight
when `add_cookies()` runs and so widens the race described here. Removing the navigation
outright would make both issues moot.
- **#41621** (closed) — produces a byte-identical
`Locator.wait_for … waiting for locator(".standalone")` error but is a different root cause
(uncommitted dashboard permalink → **404**, only on a dashboard's first-ever report run, and
only when `ALERT_REPORT_TABS` is enabled). Noting it because the error string alone does not
distinguish the two: check whether the screenshot URL returned **404** (that issue) or
**302 to `/login/`** (this issue).

### Checklist

- [x] I have searched Superset docs and Slack and didn't find a solution to my problem.
- [x] I have searched the GitHub issue tracker and didn't find a similar bug report.
- [x] I have checked Superset's logs for errors and if I found a relevant Python stacktrace, I included it here as text in the "additional context" section.

### Checklist

- [x] I have searched Superset docs and Slack and didn't find a solution to my problem.
- [x] I have searched the GitHub issue tracker and didn't find a similar bug report.
- [x] I have checked Superset's logs for errors and if I found a relevant Python stacktrace, I included it here as text in the "additional context" section.

Contributor guide

Open the contributing guide

Research direction

Start in superset/utils/machine_auth.py, especially MachineAuthProvider.authenticate_browser_context() and its Playwright browser-context setup. Check the authentication flow and report screenshot path described in the issue; done means the injected session cookie remains authenticated and screenshots reach the dashboard instead of redirecting to /login/.

Written by the indexing model from the issue text.

Assessment

Tech stack
playwright, python
Domain
authentication, backend
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.