Skip to content

Conversation

@velochy
Copy link
Contributor

@velochy velochy commented Oct 4, 2025

Describe your changes

Currently, st.logout ignores end_session_endpoint (ESE) in the OIDC configuration.
This can cause issues, as some OIDC providers keep their own cookies and skip the login screen if they have an active session.

This change:
a) Writes the name of provider into the session cookie (so it would be clear which provider the user is currently logged into)
b) Makes st.logout redirect to ESE if one is provided

This is a revised version of #11901 to adress the issues raised in #12144 (comment)
Most notably:

  1. client_id is added to ESE request (original issue, with attempted fix at Add client_id to OIDC logout request #12182)
  2. id_token_hint is also added (addressing st.logout() doesn't redirect to correct Cognito ESE #12144 (comment))
  3. ESE request redirect target it changed to /oauth2callback (adressing st.logout() doesn't redirect to correct Cognito ESE #12144 (comment))

This is a follow-up to #12044

GitHub Issue Link (if applicable)

Closes #11900

Testing Plan

  • unit tests added both for a case where ESE is provided as well as when not

Contribution License Agreement

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

@snyk-io
Copy link
Contributor

snyk-io bot commented Oct 4, 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.

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

Adds support for OIDC provider logout by redirecting to the end_session_endpoint when available, and introduces infrastructure to expose selected OAuth tokens to app code via a new st.user.tokens API. Key changes include splitting user auth data and tokens into separate signed cookies, updating logout / callback flows, and adding token exposure and access utilities with accompanying tests.

  • Implement provider-aware logout (uses end_session_endpoint with client_id, post_logout_redirect_uri, optional id_token_hint).
  • Add tokens cookie, exposure configuration (expose_tokens), TokensProxy, and st.user.tokens accessor.
  • Expand test coverage for logout flows, callback behavior, token exposure, and configuration parsing.

Reviewed Changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
lib/streamlit/web/server/server_util.py Adds constant for separate tokens cookie.
lib/streamlit/web/server/oauth_authlib_routes.py Implements provider logout logic, splits auth vs token cookies, and augments callback handling.
lib/streamlit/web/server/browser_websocket_handler.py Reads tokens cookie and filters exposed tokens per configuration.
lib/streamlit/user_info.py Adds TokensProxy and st.user.tokens API; updates logout docstring.
lib/streamlit/auth_util.py Adds get_expose_tokens_config helper.
lib/tests/streamlit/web/server/oauth_authlib_routes_test.py Adds tests for logout behavior with/without end_session_endpoint and id_token_hint.
lib/tests/streamlit/user_info_test.py Adds tests for TokensProxy and st.user.tokens behavior.
lib/tests/streamlit/auth_util_test.py Adds tests for expose_tokens config parsing.

Comment on lines 93 to 128
"""
auth_section = get_secrets_auth_section()
expose_tokens = auth_section.get("expose_tokens")

if expose_tokens is None:
return []

if isinstance(expose_tokens, str):
return [expose_tokens]
if isinstance(expose_tokens, list):
return [str(token) for token in expose_tokens]
return []


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 function accepts arbitrary token labels which (when combined with later f"{token_type}_token" logic) allows exposing sensitive refresh_token values if the config includes "refresh". Consider restricting to an explicit allowlist (e.g. {"id", "access"}) or requiring an explicit opt-in flag for refresh tokens to reduce risk of leaking long-lived credentials.

Suggested change
"""
auth_section = get_secrets_auth_section()
expose_tokens = auth_section.get("expose_tokens")
if expose_tokens is None:
return []
if isinstance(expose_tokens, str):
return [expose_tokens]
if isinstance(expose_tokens, list):
return [str(token) for token in expose_tokens]
return []
Only allows safe token types ("id", "access") by default.
To expose "refresh" tokens, require explicit opt-in via expose_refresh_token = true.
"""
ALLOWED_TOKEN_TYPES = {"id", "access"}
auth_section = get_secrets_auth_section()
expose_tokens = auth_section.get("expose_tokens")
expose_refresh_token = auth_section.get("expose_refresh_token", False)
tokens: list[str] = []
if expose_tokens is None:
tokens = []
elif isinstance(expose_tokens, str):
tokens = [expose_tokens]
elif isinstance(expose_tokens, list):
tokens = [str(token) for token in expose_tokens]
else:
tokens = []
# Filter tokens to only allow those in the allowlist
filtered_tokens = [token for token in tokens if token in ALLOWED_TOKEN_TYPES]
# Only add "refresh" if explicit opt-in is set
if expose_refresh_token and "refresh" in tokens:
filtered_tokens.append("refresh")
return filtered_tokens

Copilot uses AI. Check for mistakes.
Comment on lines +214 to +249
except Exception as e:
_LOGGER.warning("Failed to get provider logout URL: %s", e)
return None
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.

Broad except Exception masks unexpected errors (e.g. network/config issues) that could prevent proper logout diagnostics; narrow this to the specific exceptions you expect (e.g. json.JSONDecodeError, TypeError) or re-raise after logging for non-handled types.

Suggested change
except Exception as e:
_LOGGER.warning("Failed to get provider logout URL: %s", e)
return None
except (json.JSONDecodeError, TypeError, AttributeError) as e:
_LOGGER.warning("Failed to get provider logout URL: %s", e)
return None
except Exception as e:
_LOGGER.error("Unexpected error in provider logout URL: %s", e)
raise

Copilot uses AI. Check for mistakes.

@property
def tokens(self) -> TokensProxy:
"""Access exposed tokens via a dict-like object."""
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.

Expand the property docstring to document how to configure expose_tokens, list returned token name mappings (e.g. tokens.id -> id_token), and emphasize immutability and security considerations.

Suggested change
"""Access exposed tokens via a dict-like object."""
"""
Access exposed authentication tokens via a dict-like object.
Configuration:
The tokens exposed here are controlled by the `expose_tokens` configuration
option in your Streamlit app's settings. For example, you can specify which
tokens to expose by setting `expose_tokens = ["id_token", "access_token"]`
in your configuration file.
Token name mappings:
The returned object maps token names to their values. For example:
- `tokens.id` or `tokens["id"]` returns the value of the `id_token`
- `tokens.access` or `tokens["access"]` returns the value of the `access_token`
The available keys depend on your authentication provider and configuration.
Immutability and security:
The returned `TokensProxy` object is immutable; tokens cannot be modified
through this interface. Tokens are sensitive credentialsdo not log, print,
or share them. Handle with care to avoid accidental exposure.
Returns
-------
TokensProxy
A dict-like object containing the exposed authentication tokens.
"""

Copilot uses AI. Check for mistakes.
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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@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 OIDCLogoutV2 branch 3 times, most recently from 1a5c65d to 4119a63 Compare December 30, 2025 11:21
@velochy
Copy link
Contributor Author

velochy commented Dec 30, 2025

@jrieke @kmcgrady this is a follow-up to #12044 that also fixes logout to also properly log out from the oidc provider instead of just deleting the cookie.

I just rebased it to the current development branch, so it should now be ready for review. It's probably better to take this on for you while you still remember the OIDC context :)

@jrieke
Copy link
Collaborator

jrieke commented Dec 31, 2025

Just for clarification: end_session_endpoint is something returned by the OIDC provider, the developer doesn't need to set that manually in their secrets.toml, correct? I.e. this PR doesn't affect our public API in any way, it's only an internal behavior change?

@velochy
Copy link
Contributor Author

velochy commented Dec 31, 2025

Just for clarification: end_session_endpoint is something returned by the OIDC provider, the developer doesn't need to set that manually in their secrets.toml, correct? I.e. this PR doesn't affect our public API in any way, it's only an internal behavior change?

Yes, this is something that comes directly from OIDC server metadata.

This does affect behavior of course, by properly logging the user out from the OIDC provider perspective as well.

For instance, Frontegg (the provider we use) normally keeps it's on cookie and auto-logs you in when directed to login redirect with that cookie persent. Calling the logout properly clears it's cookie as well, meaning next time you log in, you are presented with a login screen. Which is the expected behavior (and my reason for this PR). I imagine it's the same for many other providers as well.

@jrieke jrieke added status:product-approved Community PR is approved by product team and removed status:needs-product-approval PR requires product approval before merging labels Jan 10, 2026
@sfc-gh-lwilby sfc-gh-lwilby added the ready-for-maintainer-review This PR is ready for maintainer attention to conduct final review before merge! label Jan 12, 2026
@kmcgrady kmcgrady added the security-assessment-completed Security assessment has been completed for PR label Jan 12, 2026
@github-actions
Copy link
Contributor

Summary

This PR implements OIDC-compliant logout functionality by redirecting users to the OAuth provider's end_session_endpoint when available. Key changes include:

  1. Modified logout flow: st.logout() now performs a proper OIDC logout by redirecting to the provider's logout endpoint with client_id, post_logout_redirect_uri, and optionally id_token_hint.
  2. Provider tracking: The auth cookie now includes the provider field to identify which OIDC provider the user logged in with.
  3. Graceful fallback: When end_session_endpoint is not available (e.g., Google), falls back to local logout only.
  4. Callback handling: The /oauth2callback endpoint now gracefully handles missing state parameters (for logout redirects) by returning 302 instead of 400.

This addresses the issue where OIDC providers that maintain their own session cookies would auto-login users even after calling st.logout().

Code Quality

Strengths

  1. Well-structured code: The new methods _get_provider_logout_url() and _get_redirect_uri() are properly encapsulated with clear single responsibilities.

  2. Good logging practices: Appropriate use of logging levels (_LOGGER.info for expected scenarios, _LOGGER.warning for failures, _LOGGER.exception for errors).

  3. Proper error handling: The code gracefully handles various failure scenarios (missing cookies, missing provider, invalid JSON, network errors) without breaking the logout flow.

  4. Security considerations:

    • Redirect URI is validated to end with /oauth2callback before use
    • URL parameters are properly encoded with urlencode()
    • Cookie values are signed

Minor Observations

  1. Line 285-293 in oauth_authlib_routes.py: There's a redundant if provider is None: check in AuthCallbackHandler.get() after the check at lines 253-258. The code path shows this cannot be reached since we return early at line 258. However, looking at the comment referencing issue "Error, missing provider for callback." - phantom error log on st.logout() #13101, this appears to be intentional defensive code for edge cases. Consider simplifying this.
        if provider is None:
            # See https://github.com/streamlit/streamlit/issues/13101
            _LOGGER.warning(
                "Missing provider for OAuth callback; this often indicates a stale "
                "or replayed callback (for example, from browser back/forward "
                "navigation).",
            )
            self.redirect_to_base()
            return
  1. Exception handling in _get_provider_logout_url(): The broad except Exception as e: at line 246 catches all exceptions. While this is acceptable for a best-effort feature, consider being more specific or adding debug logging for unexpected exception types.

Test Coverage

Strengths

The unit test coverage is comprehensive and well-structured:

  1. test_logout_success_no_cookie: Tests fallback behavior when no auth cookie exists.
  2. test_logout_with_oidc_end_session_endpoint: Tests redirect to provider's logout URL with required parameters.
  3. test_logout_with_id_token_hint: Tests that id_token_hint is correctly included when available.
  4. test_logout_fallback_no_end_session_endpoint: Tests graceful fallback when provider doesn't support OIDC logout.
  5. Updated callback test: Verifies provider field is stored in cookie.
  6. Updated missing state test: Verifies 302 redirect instead of 400 error.

Potential Test Gaps

The following scenarios are not covered but would be good additions:

  1. Invalid JSON in auth cookie: What happens if the cookie value is corrupted?
  2. Exception during load_server_metadata(): Network failures or timeouts
  3. Missing redirect_uri in secrets: Edge case when secrets are misconfigured
  4. id_token is present but empty/invalid: Edge case with tokens cookie

These are minor and the current tests adequately cover the main functionality.

Backwards Compatibility

This PR is backwards compatible:

  1. Additive cookie change: Adding provider to the auth cookie is non-breaking. Existing cookies without provider will simply not trigger OIDC logout.

  2. Graceful degradation: If end_session_endpoint is unavailable, the original local logout behavior is preserved.

  3. Behavior change for callback endpoint: The /oauth2callback endpoint now returns 302 instead of 400 when state is missing. This is intentional and necessary to support the OIDC logout redirect flow.

  4. Documentation updated: The st.logout() docstring correctly reflects the new behavior.

Security & Risk

Security Assessment

  1. ✅ No sensitive data exposure: The id_token passed to the logout endpoint is per OIDC RP-Initiated Logout specification and is sent directly to the identity provider.

  2. ✅ URL injection prevention: Using urlencode() properly escapes parameters.

  3. ✅ Redirect URI validation: The code validates that redirect_uri ends with /oauth2callback before use.

  4. ✅ Signed cookies: Auth and tokens cookies remain signed and httpOnly.

Risk Assessment

  1. Low Risk: If the provider's end_session_endpoint is unreachable, the user's local session will be cleared but they may remain logged in at the provider. This is an acceptable trade-off documented in the updated docstring.

  2. No E2E tests: The OIDC logout flow involves browser redirects that are difficult to test without E2E tests. The unit tests cover the server-side logic well, but integration testing would require manual verification or dedicated E2E infrastructure for OAuth testing.

Recommendations

  1. Consider adding defensive test for malformed cookies: Add a unit test for the scenario where json.loads(cookie_value) fails.

  2. Documentation: The docstring change accurately describes the new behavior. Good job on updating user-facing documentation.

  3. No blocking issues identified: The implementation follows OIDC best practices and handles edge cases appropriately.

Verdict

APPROVED: This PR correctly implements OIDC-compliant logout functionality with proper fallback behavior, comprehensive unit tests, and good security practices. The change is backwards compatible and improves the user experience for OIDC providers that support end_session_endpoint. The code quality is high, and the logging/error handling is appropriate for production use.


This is an automated AI review. Please verify the feedback and use your judgment.

Copy link
Collaborator

@kmcgrady kmcgrady left a comment

Choose a reason for hiding this comment

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

Overall the changes look good @velochy just one question from me before I merge. I rebased off the latest and pushed a recommended test.

redirect_uri: str = auth_section["redirect_uri"]
if not redirect_uri.endswith("/oauth2callback"):
_LOGGER.warning("Redirect URI does not end with /oauth2callback")
return None
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we need to include the port logic here @velochy from the other PR as well. Probably should consolidate and use redirect_uri = get_redirect_uri(auth_section), which handles that case, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are correct. Good catch :)

Copy link
Collaborator

@kmcgrady kmcgrady Jan 12, 2026

Choose a reason for hiding this comment

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

Haha I try. I'll add the change :-). This should make the 1.53 release if I can get it merged today.

@velochy Do the changes look good to you?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

reluctant to comment on the removed dead code, but the other two changes look good :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yea, we check provider is None twice. The first time is return so the dead code is unreachable (our static analysis found this)

@kmcgrady kmcgrady merged commit e83ef3a into streamlit:develop Jan 12, 2026
40 checks passed
lukasmasuch added a commit that referenced this pull request Jan 17, 2026
## Describe your changes

Implementing auth-related features from
#12693 and
#12693 for the new Starlette
implementation. This also applies some refactoring for better reuse and
adds an e2e test.

## Testing Plan

- Added unit and e2e tests.

---

**Contribution License Agreement**

By submitting this pull request you agree that all contributions to this
project are made under the Apache 2.0 license.
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 ready-for-maintainer-review This PR is ready for maintainer attention to conduct final review before merge! security-assessment-completed Security assessment has been completed for PR status:product-approved Community PR is approved by product team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

st.logout does not end the session with the OIDC provider

6 participants