2828import threading
2929import time
3030from collections .abc import Callable
31+ from urllib .parse import quote as _urlquote
3132from urllib .parse import urlencode , urlparse
3233
3334import falcon
@@ -248,11 +249,11 @@ def _exchange_code_for_token(
248249 client_id : str ,
249250 client_secret : str | None ,
250251 use_id_token : bool ,
251- ) -> tuple [str , int ]:
252+ ) -> tuple [str , int , str | None ]:
252253 """Exchange an authorization code for a token.
253254
254255 Returns:
255- ``(token, max_age_seconds)``
256+ ``(token, max_age_seconds, refresh_token_or_none )``
256257
257258 Raises:
258259 ValueError: On token exchange failure.
@@ -276,6 +277,8 @@ def _exchange_code_for_token(
276277 except Exception as exc :
277278 raise ValueError (f"Token exchange failed: { exc } " ) from exc
278279
280+ refresh_token = body .get ("refresh_token" )
281+
279282 if use_id_token :
280283 token = body .get ("id_token" )
281284 if not token :
@@ -291,16 +294,16 @@ def _exchange_code_for_token(
291294 exp = claims .get ("exp" )
292295 if exp is not None :
293296 max_age = max (int (exp ) - int (time .time ()), 60 )
294- return token , max_age
297+ return token , max_age , refresh_token
295298 except Exception :
296299 pass
297- return token , _AUTH_COOKIE_DEFAULT_MAX_AGE
300+ return token , _AUTH_COOKIE_DEFAULT_MAX_AGE , refresh_token
298301 else :
299302 token = body .get ("access_token" )
300303 if not token :
301304 raise ValueError ("Token response missing access_token" )
302305 expires_in = body .get ("expires_in" , _AUTH_COOKIE_DEFAULT_MAX_AGE )
303- return token , int (expires_in )
306+ return token , int (expires_in ), refresh_token
304307
305308
306309# ---------------------------------------------------------------------------
@@ -506,7 +509,7 @@ def on_get(self, req: falcon.Request, resp: falcon.Response) -> None:
506509
507510 # Exchange code for token
508511 try :
509- token , max_age = _exchange_code_for_token (
512+ token , max_age , refresh_token = _exchange_code_for_token (
510513 token_endpoint = token_endpoint ,
511514 code = code ,
512515 redirect_uri = self ._redirect_uri ,
@@ -528,10 +531,19 @@ def on_get(self, req: falcon.Request, resp: falcon.Response) -> None:
528531
529532 logger .info ("OAuth PKCE authentication successful" )
530533
531- # External frontend: redirect with token in URL fragment (no cookie needed)
534+ # External frontend: redirect with token + OAuth metadata in URL fragment
532535 if return_to :
533536 separator = "#" if "#" not in return_to else "&"
534- redirect_url = f"{ return_to } { separator } token={ token } "
537+ fragment_params = [f"token={ token } " ]
538+ if refresh_token :
539+ fragment_params .append (f"refresh_token={ _urlquote (refresh_token )} " )
540+ fragment_params .append (f"token_endpoint={ _urlquote (token_endpoint )} " )
541+ fragment_params .append (f"client_id={ _urlquote (self ._client_id )} " )
542+ if self ._client_secret :
543+ fragment_params .append (f"client_secret={ _urlquote (self ._client_secret )} " )
544+ if self ._use_id_token :
545+ fragment_params .append ("use_id_token=true" )
546+ redirect_url = f"{ return_to } { separator } { '&' .join (fragment_params )} "
535547 logger .info ("OAuth redirecting to external frontend: %s" , return_to .split ("?" )[0 ])
536548 resp .status = "302 Found"
537549 resp .set_header ("Location" , redirect_url )
0 commit comments