Skip to content

Commit d63343e

Browse files
rustyconoverclaude
andcommitted
Pass refresh_token and OAuth metadata in external frontend redirect fragment (v0.6.7)
Token exchange now returns refresh_token. External frontend redirects include refresh_token, token_endpoint, client_id, client_secret, and use_id_token in the URL fragment so frontends can refresh tokens independently. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 27965ad commit d63343e

File tree

3 files changed

+22
-10
lines changed

3 files changed

+22
-10
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "vgi-rpc"
3-
version = "0.6.6"
3+
version = "0.6.7"
44
description = "Vector Gateway Interface - RPC framework based on Apache Arrow"
55
readme = "README.md"
66
requires-python = ">=3.13"

tests/test_oauth_pkce.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,7 @@ def test_successful_callback_redirects_with_cookie(self, mock_client_cls, mock_e
577577
mock_client_cls.return_value = mock_client
578578

579579
token = _mint_jwt(self.priv)
580-
mock_exchange.return_value = (token, 3600)
580+
mock_exchange.return_value = (token, 3600, None)
581581

582582
state = "test-state-123"
583583
cookie = self._make_session_cookie(state=state, url="/vgi/describe")

vgi_rpc/http/_oauth_pkce.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import threading
2929
import time
3030
from collections.abc import Callable
31+
from urllib.parse import quote as _urlquote
3132
from urllib.parse import urlencode, urlparse
3233

3334
import 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

Comments
 (0)