Machine auth's /login/ priming page can clobber the injected session cookie, sending screenshots to the login screen
- 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
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