@@ -324,16 +324,43 @@ def _validate_original_url(url: str, prefix: str) -> str:
324324 return url
325325
326326
327- def _validate_return_to (url : str ) -> str :
328- """Validate an external return-to URL. Returns empty string if invalid."""
327+ def _is_localhost (hostname : str ) -> bool :
328+ """Check if a hostname is localhost (any port)."""
329+ return hostname in ("localhost" , "127.0.0.1" , "[::1]" )
330+
331+
332+ # Default origins allowed for _vgi_return_to redirects.
333+ _DEFAULT_ALLOWED_RETURN_ORIGINS : frozenset [str ] = frozenset (("https://cupola.query-farm.services" ,))
334+
335+
336+ def _validate_return_to (url : str , allowed_origins : frozenset [str ] = frozenset ()) -> str :
337+ """Validate an external return-to URL against an origin allowlist.
338+
339+ Returns the URL if it matches an allowed origin or is localhost,
340+ otherwise returns empty string. Only the scheme and host (ignoring
341+ port for localhost) are checked — any path is permitted.
342+ """
329343 if not url or len (url ) > 2048 :
330344 return ""
331345 parsed = urlparse (url )
332346 if parsed .scheme not in ("http" , "https" ):
333347 return ""
334348 if not parsed .netloc :
335349 return ""
336- return url
350+ # localhost with any port is always allowed
351+ hostname = parsed .hostname or ""
352+ if _is_localhost (hostname ) and parsed .scheme == "http" :
353+ return url
354+ # Check against allowlist (scheme + host, ignoring path)
355+ origin = f"{ parsed .scheme } ://{ parsed .hostname } "
356+ if origin in allowed_origins :
357+ return url
358+ # Also try with explicit port
359+ if parsed .port :
360+ origin_with_port = f"{ parsed .scheme } ://{ parsed .hostname } :{ parsed .port } "
361+ if origin_with_port in allowed_origins :
362+ return url
363+ return ""
337364
338365
339366# ---------------------------------------------------------------------------
@@ -638,6 +665,7 @@ class _OAuthPkceMiddleware:
638665 """
639666
640667 __slots__ = (
668+ "_allowed_return_origins" ,
641669 "_client_id" ,
642670 "_oidc_discovery" ,
643671 "_prefix" ,
@@ -656,6 +684,7 @@ def __init__(
656684 secure_cookie : bool ,
657685 redirect_uri : str ,
658686 scope : str = "openid email" ,
687+ allowed_return_origins : frozenset [str ] | None = None ,
659688 ) -> None :
660689 self ._session_key = session_key
661690 self ._oidc_discovery = oidc_discovery
@@ -664,6 +693,39 @@ def __init__(
664693 self ._secure_cookie = secure_cookie
665694 self ._redirect_uri = redirect_uri
666695 self ._scope = scope
696+ self ._allowed_return_origins = (
697+ allowed_return_origins if allowed_return_origins is not None else _DEFAULT_ALLOWED_RETURN_ORIGINS
698+ )
699+
700+ def process_request (self , req : falcon .Request , resp : falcon .Response ) -> None :
701+ """If the user is already authenticated and has _vgi_return_to, redirect immediately.
702+
703+ This relies on _AuthMiddleware running first (Falcon processes
704+ process_request in middleware list order). If the cookie token is
705+ expired or invalid, _AuthMiddleware raises 401 *before* we get here,
706+ so we only redirect when the token is genuinely valid.
707+ """
708+ if req .method != "GET" :
709+ return
710+ return_to = _validate_return_to (req .get_param ("_vgi_return_to" ) or "" , self ._allowed_return_origins )
711+ if not return_to :
712+ return
713+ # Check for existing auth token in cookie
714+ token = req .cookies .get (_AUTH_COOKIE_NAME )
715+ if not token :
716+ return # Not authenticated — let normal flow handle it
717+ # Already authenticated with a return_to — redirect back with the token
718+ separator = "#" if "#" not in return_to else "&"
719+ fragment_params = [f"token={ token } " ]
720+ redirect_url = f"{ return_to } { separator } { '&' .join (fragment_params )} "
721+ logger .info ("OAuth already authenticated, redirecting to external frontend: %s" , return_to .split ("?" )[0 ])
722+ raise falcon .HTTPStatus (
723+ "302 Found" ,
724+ headers = {
725+ "Location" : redirect_url ,
726+ "Cache-Control" : "no-cache, no-store, must-revalidate" ,
727+ },
728+ )
667729
668730 def process_response (
669731 self ,
@@ -711,7 +773,7 @@ def process_response(
711773 original_url = _validate_original_url (original_url , self ._prefix )
712774
713775 # Check for external frontend return URL
714- return_to = _validate_return_to (req .get_param ("_vgi_return_to" ) or "" )
776+ return_to = _validate_return_to (req .get_param ("_vgi_return_to" ) or "" , self . _allowed_return_origins )
715777
716778 # Pack session cookie
717779 cookie_value = _pack_oauth_cookie (
0 commit comments