Skip to content

Conversation

@velochy
Copy link
Contributor

@velochy velochy commented Oct 5, 2025

Describe your changes

Add a command (st.user.refresh()) to use the refresh token to refresh both the tokens and the user info

This was separated out of #12044 at the request of maintainers.

Needs a product decision on where to place the command - either top level st.refresh_user() or st.user.refresh() (or some third option).

GitHub Issue Link (if applicable)

#12043

Testing Plan

  • Unit tests present
  • Manually tested to work for my own use case

Contribution License Agreement

By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.

@velochy velochy requested a review from a team as a code owner October 5, 2025 08:37
@snyk-io
Copy link
Contributor

snyk-io bot commented Oct 5, 2025

Snyk checks have passed. No issues have been found so far.

Status Scanner Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

except KeyError:
raise AttributeError(f'Token "{key}" is not exposed or does not exist.')

def __setattr__(self, name: str, value: str | None) -> NoReturn:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The __setattr__ method parameter value should have type annotation Any instead of str | None. The __setattr__ method can receive values of any type during attribute assignment, so the type annotation should reflect this broader scope rather than restricting it to strings and None.

Suggested change
def __setattr__(self, name: str, value: str | None) -> NoReturn:
def __setattr__(self, name: str, value: Any) -> NoReturn:

Spotted by Diamond (based on custom rule: Python Guide)

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

@velochy velochy mentioned this pull request Oct 5, 2025
@sfc-gh-lmasuch sfc-gh-lmasuch requested a review from Copilot October 6, 2025 18:27
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds OIDC token refresh functionality to Streamlit, allowing users to refresh their authentication tokens and user information without requiring a full re-login. The implementation includes a new st.user.refresh() command and corresponding backend infrastructure.

Key changes:

  • Adds AuthRefreshHandler to handle token refresh requests at /auth/refresh endpoint
  • Implements token storage in separate cookies with configurable token exposure
  • Enhances logout functionality to support provider-level logout via OIDC end_session_endpoint

Reviewed Changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
lib/streamlit/web/server/oauth_authlib_routes.py Core implementation of refresh handler and enhanced logout with provider logout support
lib/streamlit/user_info.py Adds TokensProxy class and refresh() method to user interface
lib/streamlit/auth_util.py Adds configuration parsing for token exposure settings
lib/streamlit/web/server/server.py Registers new refresh endpoint route
lib/streamlit/web/server/server_util.py Adds constant for tokens cookie name
lib/streamlit/web/server/browser_websocket_handler.py Integrates token filtering and exposure in WebSocket handler
lib/tests/streamlit/web/server/oauth_authlib_routes_test.py Comprehensive test coverage for refresh and enhanced logout functionality
lib/tests/streamlit/user_info_test.py Tests for TokensProxy and user refresh method
lib/tests/streamlit/auth_util_test.py Tests for token exposure configuration parsing


from __future__ import annotations

import json
Copy link

Copilot AI Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate import of 'json' module. The module is imported again on line 36.

Copilot uses AI. Check for mistakes.

def test_auth_callback_failure_missing_state(self):
"""Test auth callback failure missing state."""
"""Test auth callback redirects to base when state is missing (logout redirect)."""
Copy link

Copilot AI Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test behavior has changed from returning a 400 error to redirecting with 302. This change should be documented in the test comment to explain why missing state now results in a redirect instead of an error.

Suggested change
"""Test auth callback redirects to base when state is missing (logout redirect)."""
"""
Test auth callback redirects to base when state is missing (logout redirect).
Previously, missing state resulted in a 400 error. Now, the handler treats missing state
as a logout redirect and returns a 302 redirect to the base URL ("/") instead of an error.
This change ensures a smoother user experience when the state parameter is absent.
"""

Copilot uses AI. Check for mistakes.
self.set_serialized_cookie(TOKENS_COOKIE_NAME, tokens)

def set_serialized_cookie(self, cookie_name: str, value: dict[str, Any]) -> None:
"""Set a serialized cookie with a value that is less than 4096 bytes."""
Copy link

Copilot AI Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring suggests the method enforces a 4096-byte limit, but the implementation only logs an error without preventing the cookie from being set. The docstring should clarify that this method logs a warning for oversized cookies but still sets them.

Suggested change
"""Set a serialized cookie with a value that is less than 4096 bytes."""
"""
Set a serialized cookie. Logs an error if the serialized value exceeds 4096 bytes,
but still sets the cookie. Browsers may reject cookies larger than 4096 bytes.
"""

Copilot uses AI. Check for mistakes.
self, client: TornadoOAuth2App, id_token: str
) -> dict[str, Any]:
jwks_uri = client.server_metadata.get("jwks_uri")
jwks = requests.get(jwks_uri, timeout=10).json()
Copy link

Copilot AI Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling for the JWKS URI request. If the request fails or returns invalid JSON, the method will raise an unhandled exception. Consider adding try-catch to provide more informative error messages.

Suggested change
jwks = requests.get(jwks_uri, timeout=10).json()
try:
response = requests.get(jwks_uri, timeout=10)
response.raise_for_status()
jwks = response.json()
except requests.RequestException as e:
_LOGGER.error("Failed to fetch JWKS URI: %s", e)
raise StreamlitAuthError("Failed to fetch JWKS URI") from e
except ValueError as e:
_LOGGER.error("Invalid JWKS JSON from URI: %s", e)
raise StreamlitAuthError("Invalid JWKS JSON from URI") from e

Copilot uses AI. Check for mistakes.
Comment on lines +606 to +749
context.user_info.clear()
session_id = context.session_id

if runtime.exists():
instance = runtime.get_instance()
instance.clear_user_info_for_session(session_id)

base_path = config.get_option("server.baseUrlPath")

fwd_msg = ForwardMsg()
fwd_msg.auth_redirect.url = make_url_path(base_path, AUTH_REFRESH_ENDPOINT)
context.enqueue(fwd_msg)
Copy link

Copilot AI Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user_info is cleared before checking if the user is logged in or if a refresh token exists. This could result in clearing user information even when refresh fails, leaving the user in an inconsistent state.

Suggested change
context.user_info.clear()
session_id = context.session_id
if runtime.exists():
instance = runtime.get_instance()
instance.clear_user_info_for_session(session_id)
base_path = config.get_option("server.baseUrlPath")
fwd_msg = ForwardMsg()
fwd_msg.auth_redirect.url = make_url_path(base_path, AUTH_REFRESH_ENDPOINT)
context.enqueue(fwd_msg)
# Only clear user info and proceed if user is logged in and has a refresh token
user_info = context.user_info
is_logged_in = getattr(user_info, "is_logged_in", False)
has_refresh_token = bool(getattr(user_info, "refresh_token", None))
if is_logged_in and has_refresh_token:
user_info.clear()
session_id = context.session_id
if runtime.exists():
instance = runtime.get_instance()
instance.clear_user_info_for_session(session_id)
base_path = config.get_option("server.baseUrlPath")
fwd_msg = ForwardMsg()
fwd_msg.auth_redirect.url = make_url_path(base_path, AUTH_REFRESH_ENDPOINT)
context.enqueue(fwd_msg)

Copilot uses AI. Check for mistakes.
@lukasmasuch lukasmasuch added change:feature PR contains new feature or enhancement implementation impact:users PR changes affect end users labels Oct 8, 2025
@lukasmasuch lukasmasuch added the status:needs-product-approval PR requires product approval before merging label Nov 26, 2025
sfc-gh-kmcgrady pushed a commit that referenced this pull request Dec 22, 2025
## Describe your changes

Store the OIDC auth response in a cookie and expose them to users for
API calls

Currently, from user perspective, `st.user` gets an extra field called
"tokens" that is a dict of all the tokens that were made available -
most often the triplet of acces_token, id_token, refresh_token

This is useful for multiple use cases (see the issues below).

To achieve (a) reliably, the tokens are stored in a separate cookie from
user info. This is because cookies are limited to be <4096 bytes and I
ran into that limit in my own testing already if I combined the two
dictionaries. Additionally, both cookies can now be chunked so as a
by-product, this now supports login systems with larger userdata.

I have tested it to work in my own use case and it seems to be
performing as intended. But it is just one use case and I may well be
missing some others. so any proposals to improve this are very welcome.

Originally, also contained st.user_refresh command, but that got it's
own PR: #12696

## GitHub Issue Link (if applicable)

Closes #10378 

Exposing access_token:
#10378
Too large usedata for current login system:
#12518

Need to provide id token hint at logout:
#12144 (comment)
-> PR #12693
Need for user refresh:
#12043 -> PR
#12696

## Testing Plan

- Unit tests present
- Manually tested to work for my own use case

---

**Contribution License Agreement**

By submitting this pull request you agree that all contributions to this
project are made under the Apache 2.0 license.

---------

Co-authored-by: Ken McGrady <ken.mcgrady@gmail.com>
@velochy velochy force-pushed the OIDCRefreshCommand branch from dc3fb47 to a938678 Compare January 16, 2026 12:41
@velochy
Copy link
Contributor Author

velochy commented Jan 16, 2026

@kmcgrady should I try to address #13489 here as well? Its strongly related but different as it requires the refresh to happen automatically, not on command (which is what I myself need to update user info after it has been edited)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:feature PR contains new feature or enhancement implementation impact:users PR changes affect end users status:needs-product-approval PR requires product approval before merging

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants