fix: encode OAuth mode in state parameter — survives session loss
FlowDeck CI / test (push) Failing after 3s
FlowDeck CI / docker (push) Has been skipped

Before: oauth_mode stored only in request.session cookie
        → Lost if session expires during Gitea OAuth redirect
        → Link mode falls through to login mode → creates gitea_bruno user

After:  state format: <random>:<mode> (e.g., abc123:link)
        → Mode survives session cookie loss
        → Link mode correctly links to current local user
This commit is contained in:
2026-07-14 16:52:36 -04:00
parent f4cd301f52
commit c5ffab1e39
+13 -4
View File
@@ -117,11 +117,13 @@ async def login(request: Request, provider: str = Query("gitea")):
request.session["oauth_provider"] = provider
# Link mode: connect OAuth to current local account instead of creating new user
mode = request.query_params.get("mode", "")
# Encode auth mode in state to survive session loss during OAuth redirect
signed_state = f"{state}:{mode}" if mode else state
request.session["oauth_mode"] = mode
# Dynamic redirect URI based on incoming Host header
host = request.headers.get("host", "localhost:8080")
dynamic_redirect_uri = f"http://{host}/auth/callback"
auth_url = oauth_provider.get_authorize_url(state, redirect_uri=dynamic_redirect_uri, force_login=(mode == "link"))
auth_url = oauth_provider.get_authorize_url(signed_state, redirect_uri=dynamic_redirect_uri, force_login=(mode == "link"))
return RedirectResponse(url=auth_url, status_code=302)
@@ -232,11 +234,18 @@ async def callback(
state: str = Query(...),
):
"""Handle OAuth2 callback from Gitea."""
# Validate state
# Validate state — try exact match first, then with mode suffix
expected_state = request.session.get("oauth_state", "")
provider_name = request.session.get("oauth_provider", "gitea")
if not expected_state or state != expected_state:
return HTMLResponse("<h1>Invalid state</h1>", status_code=400)
if not expected_state:
return HTMLResponse("<h1>Invalid state: session expired</h1>", status_code=400)
# Check exact match or state:mode format
if state != expected_state:
parts = state.rsplit(":", 1)
if len(parts) == 2 and parts[0] == expected_state:
request.session["oauth_mode"] = parts[1]
else:
return HTMLResponse("<h1>Invalid state</h1>", status_code=400)
from app.auth.providers import get_provider
oauth_provider = get_provider(provider_name)