sfirke opened a new issue, #45587:
URL: https://github.com/apache/superset/issues/45587

   ### Bug description
   
   I found this issue by hand, then wrote it up with Claude AI. I edited and am 
posting this by hand having read it all. Note that this shouldn't affect 
properly secured deployments, because they're using server-side sessions as 
described in 
https://superset.apache.org/admin-docs/security/securing_superset/#session-management-security-critical
   
   That said, this was annoying me in my testing and is a regression from 
6.1.0, when it ndidn't happen. I believe this was introduced by #44605.
   
   With `AUTH_TYPE = AUTH_OAUTH` and the default cookie-based sessions, 
clicking the SSO provider button soon after the login page loads sometimes 
fails with:
   
   ```
   ERROR:flask_appbuilder.security.views:Error authorizing OAuth access token: 
mismatching_state: CSRF Warning! State not equal in request and response.
   ```
   
   The user sees the login page again with an access-denied message. Retrying 
almost always works.
   
   **Cause:** two requests set the session cookie at the same time.
   
   1. `/login/` is a frontend route, so `LocationPathnameLogger` 
(`src/views/App.tsx`) logs `spa_navigation` when it renders. 
`loggerMiddleware.ts` debounces that for 1 s, then sends it with 
`navigator.sendBeacon` to `POST /log/?explode=events`.
   2. The user isn't logged in yet, so that POST gets a 302 to 
`/login/?next=…/log/?explode=events`. Rendering that login page writes to the 
session (it creates a CSRF token) and returns a new `Set-Cookie: session=…`, 
built from the cookie the request carried, which has no OAuth state.
   3. Meanwhile, the user's click on `/login/<provider>` makes Flask-AppBuilder 
store `oauth_state` in the session cookie and redirect to the identity provider.
   4. If the beacon's redirect response is processed after the 
`/login/<provider>` response, it overwrites the cookie and `oauth_state` is 
lost. When the identity provider redirects back to 
`/oauth-authorized/<provider>`, the state doesn't match.
   
   Access log from one failed attempt followed by a successful retry (Azure AD; 
tokens redacted):
   
   ```
   19:59:22 "GET /login/?next=%2Fwelcome%2F" 200
   19:59:24 "POST /log/?explode=events" 302                      <- debounced 
spa_navigation beacon, user is anonymous
   19:59:24 "GET /login/azure?next=%2Fwelcome%2F" 302            <- user clicks 
SSO; oauth_state stored in cookie
   19:59:24 "GET /login/?next=https://<host>/log/?explode%3Devents" 200   <- 
beacon redirect; Set-Cookie without oauth_state
   19:59:25 ERROR ... mismatching_state: CSRF Warning! State not equal in 
request and response.
   19:59:25 "GET /oauth-authorized/azure?code=<…>&state=<…>" 302
   19:59:25 "GET /login/" 200
   19:59:27 "POST /log/?explode=events" 302                      <- beacon 
fires before the click this time
   19:59:27 "GET /login/?next=https://<host>/log/?explode%3Devents" 200
   19:59:29 "GET /login/azure" 302
   19:59:30 "GET /oauth-authorized/azure?code=<…>&state=<…>" 302 <- success
   19:59:30 "GET /welcome/" 200
   ```
   
   **How to reproduce:**
   1. Configure OAuth login (seen with Azure AD) and keep the default 
`SESSION_SERVER_SIDE = False`.
   2. Log out, or let the session expire.
   3. On `/login/`, click the provider button within about 1–3 s of the page 
loading.
   4. Sometimes you're returned to the login page with access denied, and the 
server logs `mismatching_state`. It depends on timing, so it doesn't happen 
every time.
   
   Logging out and logging back in hits the same window, and so does any visit 
where the session has expired (for example, the first visit of the day with a 
short `PERMANENT_SESSION_LIFETIME`).
   
   **Expected behavior:** SSO login succeeds on the first click. No `/log/` 
request should be sent from the login page, or at least none that can change 
the session.
   
   **Possible fixes:**
   - Frontend: don't send log events when the bootstrap user isn't logged in, 
or don't log `spa_navigation` for `/login/`.
   - Backend: `/log/` should return 401 to anonymous requests instead of 
redirecting to the login page, which is the step that writes the session cookie.
   
   
   ### Screenshots/recordings
   
   _No response_
   
   ### Superset version
   
   master / latest-dev
   
   ### Python version
   
   Not applicable
   
   ### Node version
   
   Not applicable
   
   ### Browser
   
   Not applicable
   
   ### Additional context
   
   **Regression in 7.0:** not reproducible on 6.1.0. Replaying the request 
sequence against both versions with identical config: on 6.1.0 the beacon's 
follow-up `GET /login/?next=…/log/…` returns no `Set-Cookie`; on 7.0.0rc2 it 
does, because #44605 now drains `_flashes` (added by the anonymous `/log/` 302) 
in the login view, which saves the session. That response is what overwrites 
`oauth_state`. #44605 fixes a real problem (unbounded cookie growth, #44541), 
so the fix probably belongs on the logger side: skip beacons for anonymous 
users, or have `/log/` return 401 instead of flash + redirect.
   
   ### 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.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to