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)