Skip to content

Conversation

@velochy
Copy link
Contributor

@velochy velochy commented Aug 18, 2025

Describe your changes

Changed functionality so that if redirect_uri hostname part ends with a colon, it adds the current running port after it.
This allows multiple streamlit dashboards to run on the same host and share a secrets.toml file - which is especially useful in the local development environment, but might also have uses in some deployment situations

GitHub Issue Link (if applicable)

#12249

Testing Plan

  • Added unit tests
  • Hopefully no additional tests needed as behavior did not change unless redirect_uri has hostname ending in a colon.

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 Aug 18, 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.

@velochy velochy changed the title Default redirect URI added Allow OAuth redirect_uri to use current port Aug 18, 2025
@velochy velochy force-pushed the DefaultRedirectURI branch from a65b59c to f29a462 Compare August 18, 2025 13:18
@velochy velochy changed the title Allow OAuth redirect_uri to use current port Allow OAuth redirect_uri to reference current port Aug 18, 2025
Comment on lines 88 to 110
def get_redirect_uri(auth_section: AttrDict) -> str:
"""Get the redirect_uri from auth_section - filling in port number if needed."""

if "redirect_uri" not in auth_section:
return None

redirect_uri = auth_section["redirect_uri"]

try:
redirect_uri_parsed = urlparse(redirect_uri)
except ValueError:
raise StreamlitAuthError(
f"Invalid redirect_uri: {redirect_uri}. Please check your configuration."
)

# If redirect_uri hostname ends with ':'
# (indicating no port number), add the port number
if redirect_uri_parsed.netloc[-1] == ":":
redirect_uri_parsed = redirect_uri_parsed._replace(
netloc=redirect_uri_parsed.netloc + str(config.get_option("server.port"))
)

return redirect_uri_parsed.geturl()
Copy link
Contributor

Choose a reason for hiding this comment

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

The get_redirect_uri function requires a complete Numpydoc-style docstring. The current single-line docstring does not comply with the Python Guide requirement that all user-facing modules have proper top-level docstrings in Numpydoc format. The docstring should include:

  • Parameters section documenting the auth_section parameter
  • Returns section describing the return value type and meaning
  • Raises section documenting the potential StreamlitAuthError exception

Replace the current docstring with a properly formatted multi-line Numpydoc docstring containing these required sections.

Suggested change
def get_redirect_uri(auth_section: AttrDict) -> str:
"""Get the redirect_uri from auth_section - filling in port number if needed."""
if "redirect_uri" not in auth_section:
return None
redirect_uri = auth_section["redirect_uri"]
try:
redirect_uri_parsed = urlparse(redirect_uri)
except ValueError:
raise StreamlitAuthError(
f"Invalid redirect_uri: {redirect_uri}. Please check your configuration."
)
# If redirect_uri hostname ends with ':'
# (indicating no port number), add the port number
if redirect_uri_parsed.netloc[-1] == ":":
redirect_uri_parsed = redirect_uri_parsed._replace(
netloc=redirect_uri_parsed.netloc + str(config.get_option("server.port"))
)
return redirect_uri_parsed.geturl()
def get_redirect_uri(auth_section: AttrDict) -> str:
"""Get the redirect_uri from auth_section - filling in port number if needed.
This function extracts the redirect_uri from the provided auth_section.
If the redirect_uri's hostname ends with a colon (indicating no port number),
the server's port number will be appended.
Parameters
----------
auth_section : AttrDict
The authentication configuration section containing redirect_uri.
Returns
-------
str or None
The processed redirect URI with port number if needed, or None if
no redirect_uri is found in auth_section.
Raises
------
StreamlitAuthError
If the redirect_uri is invalid and cannot be parsed.
"""
if "redirect_uri" not in auth_section:
return None
redirect_uri = auth_section["redirect_uri"]
try:
redirect_uri_parsed = urlparse(redirect_uri)
except ValueError:
raise StreamlitAuthError(
f"Invalid redirect_uri: {redirect_uri}. Please check your configuration."
)
# If redirect_uri hostname ends with ':'
# (indicating no port number), add the port number
if redirect_uri_parsed.netloc[-1] == ":":
redirect_uri_parsed = redirect_uri_parsed._replace(
netloc=redirect_uri_parsed.netloc + str(config.get_option("server.port"))
)
return redirect_uri_parsed.geturl()

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

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

if secrets_singleton.load_if_toml_exists():
auth_section = cast("AttrDict", secrets_singleton.get("auth"))

return auth_section


def get_redirect_uri(auth_section: AttrDict) -> str:
Copy link
Contributor

Choose a reason for hiding this comment

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

The return type annotation for get_redirect_uri is incorrect. The function returns None when 'redirect_uri' is not present in auth_section, but the return type is annotated as str. The return type should be str | None or Optional[str] to accurately reflect that the function can return either a string or None, as required by Python typing standards.

Suggested change
def get_redirect_uri(auth_section: AttrDict) -> str:
def get_redirect_uri(auth_section: AttrDict) -> Optional[str]:

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

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


# If redirect_uri hostname ends with ':'
# (indicating no port number), add the port number
if redirect_uri_parsed.netloc[-1] == ":":
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider adding a check to ensure netloc is not empty before accessing its last character. If netloc is empty, the expression netloc[-1] would raise an IndexError. A simple guard condition like if redirect_uri_parsed.netloc and redirect_uri_parsed.netloc[-1] == ":" would prevent this potential issue.

Suggested change
if redirect_uri_parsed.netloc[-1] == ":":
if redirect_uri_parsed.netloc and redirect_uri_parsed.netloc[-1] == ":":

Spotted by Diamond

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

@velochy
Copy link
Contributor Author

velochy commented Aug 18, 2025

My original idea was to just have a default redirect_uri value of http://localhost:port/oauth2Callback, but the current approach is more flexible, as it also enables more complex situations like using a virtual machine or with some other domain name being whitelisted for use at OAuth provider that is locally mapped to localhost.

It should still be relatively safe for existing users, as ':' is a special symbol in URI reserved for specifying the port.
So the only reason to end hostname with it is to add a port afterwards.

This being said, it's not hard to change it to more explicit templating, like to expect ':port' instead of just ':' if you want to be more careful

@velochy velochy force-pushed the DefaultRedirectURI branch from f29a462 to aacea1a Compare August 18, 2025 13:28
Comment on lines 76 to 123
def test_get_redirect_uri_verbatim(self):
"""Test get_redirect_uri returns the existing redirect_uri when present."""
auth_section = AttrDict({"redirect_uri": "https://example.com/callback"})
result = get_redirect_uri(auth_section)
assert result == "https://example.com/callback"

@patch(
"streamlit.auth_util.config",
MagicMock(
get_option=MagicMock(return_value=8502),
),
)
def test_get_redirect_uri_with_port_added(self):
"""Test get_redirect_uri returns default value when redirect_uri is missing."""
auth_section = AttrDict({"redirect_uri": "http://localhost:/oauth2callback"})
result = get_redirect_uri(auth_section)
assert result == "http://localhost:8502/oauth2callback"
Copy link
Contributor

Choose a reason for hiding this comment

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

The test methods test_get_redirect_uri_verbatim and test_get_redirect_uri_with_port_added are missing return type annotations. According to the Python Unit Test Guide, new tests should be fully annotated with types. Add -> None return type annotations to both test methods.

Suggested change
def test_get_redirect_uri_verbatim(self):
"""Test get_redirect_uri returns the existing redirect_uri when present."""
auth_section = AttrDict({"redirect_uri": "https://example.com/callback"})
result = get_redirect_uri(auth_section)
assert result == "https://example.com/callback"
@patch(
"streamlit.auth_util.config",
MagicMock(
get_option=MagicMock(return_value=8502),
),
)
def test_get_redirect_uri_with_port_added(self):
"""Test get_redirect_uri returns default value when redirect_uri is missing."""
auth_section = AttrDict({"redirect_uri": "http://localhost:/oauth2callback"})
result = get_redirect_uri(auth_section)
assert result == "http://localhost:8502/oauth2callback"
def test_get_redirect_uri_verbatim(self) -> None:
"""Test get_redirect_uri returns the existing redirect_uri when present."""
auth_section = AttrDict({"redirect_uri": "https://example.com/callback"})
result = get_redirect_uri(auth_section)
assert result == "https://example.com/callback"
@patch(
"streamlit.auth_util.config",
MagicMock(
get_option=MagicMock(return_value=8502),
),
)
def test_get_redirect_uri_with_port_added(self) -> None:
"""Test get_redirect_uri returns default value when redirect_uri is missing."""
auth_section = AttrDict({"redirect_uri": "http://localhost:/oauth2callback"})
result = get_redirect_uri(auth_section)
assert result == "http://localhost:8502/oauth2callback"

Spotted by Diamond (based on custom rule: Python Unit Tests)

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

@sfc-gh-bnisco sfc-gh-bnisco added change:feature PR contains new feature or enhancement implementation impact:users PR changes affect end users labels Aug 20, 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 implements functionality to allow OAuth redirect_uri configuration to dynamically reference the current port when the hostname ends with a colon. This enables multiple Streamlit applications to run on the same host using different ports while sharing the same secrets.toml configuration file.

  • Added a new get_redirect_uri function that processes redirect URIs and appends the current port when needed
  • Updated OAuth client creation and origin detection to use the new function instead of direct configuration access
  • Added comprehensive unit tests covering both standard redirect URIs and dynamic port insertion

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
lib/streamlit/auth_util.py Added get_redirect_uri function to handle dynamic port insertion for redirect URIs ending with colon
lib/streamlit/web/server/oauth_authlib_routes.py Updated OAuth client creation and origin detection to use new get_redirect_uri function
lib/tests/streamlit/auth_util_test.py Added unit tests for the new redirect URI functionality covering both standard and dynamic port scenarios

),
)
def test_get_redirect_uri_with_port_added(self):
"""Test get_redirect_uri returns default value when redirect_uri is missing."""
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 is incorrect. This test is checking port addition functionality, not handling missing redirect_uri. It should be: 'Test get_redirect_uri adds current port when hostname ends with colon.'

Suggested change
"""Test get_redirect_uri returns default value when redirect_uri is missing."""
"""Test get_redirect_uri adds current port when hostname ends with colon."""

Copilot uses AI. Check for mistakes.

# If redirect_uri hostname ends with ':'
# (indicating no port number), add the port number
if redirect_uri_parsed.netloc[-1] == ":":
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.

This code will raise an IndexError if netloc is an empty string. Add a check to ensure netloc is not empty before accessing its last character.

Suggested change
if redirect_uri_parsed.netloc[-1] == ":":
if redirect_uri_parsed.netloc and redirect_uri_parsed.netloc[-1] == ":":

Copilot uses AI. Check for mistakes.
Comment on lines +88 to +131
def get_redirect_uri(auth_section: AttrDict) -> str | None:
"""Get the redirect_uri from auth_section - filling in port number if needed."""

Copy link
Contributor

Choose a reason for hiding this comment

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

The get_redirect_uri function lacks a proper docstring. According to Python Guide docstring requirements, all functions must use Numpydoc style docstrings with triple quotes. The current single-line comment should be replaced with a comprehensive docstring including Parameters, Returns, and other relevant sections as specified by the Numpydoc format.

Suggested change
def get_redirect_uri(auth_section: AttrDict) -> str | None:
"""Get the redirect_uri from auth_section - filling in port number if needed."""
def get_redirect_uri(auth_section: AttrDict) -> str | None:
"""Get the redirect_uri from auth_section, filling in port number if needed.
Parameters
----------
auth_section : AttrDict
The authentication configuration section containing redirect URI information.
Returns
-------
str or None
The processed redirect URI with port number filled in if needed, or None if
no redirect URI is configured.
"""

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

Fix in Graphite


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

@lukasmasuch lukasmasuch added the status:needs-product-approval PR requires product approval before merging label Nov 26, 2025
@jrieke
Copy link
Collaborator

jrieke commented Dec 31, 2025

That's a great idea, agree that having this would be handy. But I feel like having a trailing colon is a bit too implicit. I think we should do this in a way where if you have {port} in redirect_uri, we replace it with the current port. So you could e.g. do:

[auth]
redirect_uri = "http://localhost:{port}/oauth2callback"

I don't think we have any precedence for the curly bracket syntax in Streamlit, but I think it rhymes well with f-strings in Python. What do you think?

@velochy
Copy link
Contributor Author

velochy commented Dec 31, 2025

colon was the minimum viable option. Agree that something more explicit is probably better :)
Changed the code to check for {port} now.

@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
@github-actions
Copy link
Contributor

Summary

This PR adds support for a {port} placeholder in the OAuth redirect_uri configuration. When specified, the placeholder is automatically replaced with the current Streamlit server port at runtime. This enables multiple Streamlit dashboards to share a single secrets.toml file while running on different ports—particularly useful for local development environments.

Changes:

  • Added new get_redirect_uri(auth_section: AttrDict) -> str | None function in lib/streamlit/auth_util.py
  • Updated oauth_authlib_routes.py to use the new function (2 locations)
  • Added 2 unit tests for the new functionality

Related issue: #12249

Code Quality

The implementation is clean, focused, and follows existing patterns in the codebase:

Positives:

  • ✅ Function has proper type annotations
  • ✅ Docstring follows Numpy style conventions
  • ✅ Uses from __future__ import annotations (existing in file)
  • ✅ Follows the pattern of other functions in auth_util.py
  • ✅ Good separation of concerns - extraction logic is in a reusable function
  • ✅ Fixed minor docstring placement issue in get_secrets_auth_section() (lines 79-81)

Minor observations:

  1. Potentially unreachable exception handler (lib/streamlit/auth_util.py, lines 100-105): The try/except ValueError around urlparse() is defensive coding, but Python's urlparse is very lenient and doesn't typically raise ValueError on invalid URLs. While not harmful, this code branch may never execute in practice. Consider either:

    • Removing it if validation isn't needed (since secrets.toml is a trusted source)
    • Adding explicit validation of the parsed URL components if validation is desired
  2. URL normalization: Using urlparse().geturl() could theoretically normalize URLs, but for typical redirect URIs this is unlikely to cause issues.

Test Coverage

Existing tests:

  • test_get_redirect_uri_verbatim: Tests that a redirect_uri without placeholders is returned unchanged ✅
  • test_get_redirect_uri_with_port_placeholder: Tests that {port} is correctly substituted ✅

Tests follow best practices:

  • ✅ Tests have descriptive docstrings
  • ✅ Proper mocking of config
  • ✅ Tests are focused and test one thing each

Suggested additional test coverage:

  1. Missing test for None return case: When redirect_uri is not present in auth_section, the function returns None. This path should have explicit test coverage:
def test_get_redirect_uri_returns_none_when_not_present(self):
    """Test get_redirect_uri returns None when redirect_uri is not in auth_section."""
    auth_section = AttrDict({"some_other_key": "value"})
    result = get_redirect_uri(auth_section)
    assert result is None
  1. Consider edge case test: Multiple {port} occurrences in the URL (though unusual, str.replace() handles it correctly).

Backwards Compatibility

Fully backwards compatible

  • Existing configurations without {port} placeholders work identically to before
  • The {port} placeholder is opt-in—users must explicitly include it to activate the new behavior
  • The function returns the same value for URLs without placeholders (verified by test_get_redirect_uri_verbatim)

Security & Risk

No security concerns identified

  • The redirect_uri value comes from secrets.toml, which is a trusted configuration source controlled by the app owner
  • The port value comes from Streamlit's server configuration, also controlled by the app owner
  • No user-supplied input is involved in this feature
  • URL parsing via urlparse is safe and well-tested

Risk assessment: Low

  • The change is small and localized
  • It only affects OAuth redirect URI handling
  • The feature is opt-in via explicit placeholder usage

Recommendations

  1. [Optional] Add a test for the None return case when redirect_uri is not present in auth_section. This ensures complete branch coverage.

  2. [Optional] Consider removing or simplifying the ValueError exception handling around urlparse() since it's unlikely to be triggered. If validation is desired, explicit checks on parsed URL components would be more effective.

  3. [Documentation] The PR description mentions "hostname ending in a colon" but the implementation uses {port} placeholder. The implementation is cleaner than described—ensure documentation accurately reflects the {port} placeholder syntax.

Verdict

APPROVED: This is a well-implemented, backwards-compatible feature that solves a real use case for developers running multiple Streamlit dashboards locally. The code follows existing patterns, has reasonable test coverage, and introduces no security risks. The minor suggestions above are optional improvements and do not block merging.


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

@kmcgrady kmcgrady added the security-assessment-completed Security assessment has been completed for PR label Jan 12, 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 merged commit dbf6193 into streamlit:develop Jan 12, 2026
49 checks passed
@lukasmasuch
Copy link
Collaborator

lukasmasuch commented Jan 12, 2026

A small callout: we are currently in the process of migrating the server from Tornado to Starlette+Uvicorn. 1.53 will come with optional experimental Starlette support, and we likely will fully migrate to Starlette next quarter. The annoying part here is that for aspects impacting the server itself - oauth_authlib_routes in this case - will need to also be applied to the Starlette implementation, probably in starlette_auth_routes.py.

@velochy
Copy link
Contributor Author

velochy commented Jan 12, 2026

A small callout: we are currently in the process of migrating the server from Tornado to Starlette+Uvicorn. 1.53 will come with optional experimental Starlette support, and we likely will fully migrate to Starlette next quarter. The annoying part here is that for aspects impacting the server itself - oauth_authlib_routes in this case - will need to also be applied to the Starlette implementation, probably in starlette_auth_routes.py.

This is something AI can likely do pretty well. Possibly you can have it do all the commits from a period in a batch instead of individually, as it might be faster to both ask for and to review (assuming the individual changes are not too big)

@lukasmasuch
Copy link
Collaborator

Yep, I agree that this is easy to do with AI. I think one issue is that we don't have end-to-end (e2e) tests for these changes, which makes it hard to actually detect the relevant changes. I know this is quite hard to properly cover e2e, but is there any way to add some coverage similar to how it's done in https://github.com/streamlit/streamlit/blob/develop/e2e_playwright/auth.py and https://github.com/streamlit/streamlit/blob/develop/e2e_playwright/auth_test.py

@lukasmasuch
Copy link
Collaborator

@velochy I prepared a PR to add both of the new features + a basic e2e test: #13571 Can you check if this looks fine?

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.

6 participants