From c5ffab1e399835cb5e3d97702c912984fd7aa418 Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 14 Jul 2026 16:52:36 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20encode=20OAuth=20mode=20in=20state=20par?= =?UTF-8?q?ameter=20=E2=80=94=20survives=20session=20loss?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: : (e.g., abc123:link) → Mode survives session cookie loss → Link mode correctly links to current local user --- app/routers/auth.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/app/routers/auth.py b/app/routers/auth.py index efe77fa..1cb5925 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -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("

Invalid state

", status_code=400) + if not expected_state: + return HTMLResponse("

Invalid state: session expired

", 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("

Invalid state

", status_code=400) from app.auth.providers import get_provider oauth_provider = get_provider(provider_name)