Migrating to JWT and OIDC¶
This guide assumes you already run FluxLit with public API routes under /api and Streamlit on the same origin.
Step 1: Install the auth extra¶
pip install "fluxlit[auth]"
Step 2: Protect a few API routes¶
2b. Attach to routes¶
from typing import Annotated
from fastapi import Depends
from fluxlit import FluxLit
from fluxlit.auth.jwt import StandardClaims
app = FluxLit(title="My app")
@app.api.get("/me")
def me(claims: Annotated[StandardClaims, Depends(_bearer)]):
return {"sub": claims.sub}
@app.api.get("/healthz") # already registered by FluxLit on the inner API
def _note() -> None:
"""Use the built-in health route or add public routes without Depends(_bearer)."""
2c. Optional: scopes / roles¶
from fluxlit.auth.jwt import RequireScopes
_need_read = RequireScopes(_bearer, "data.read")
@app.api.get("/v1/widgets")
def list_widgets(_: Annotated[StandardClaims, Depends(_need_read)]):
return []
Leave public bootstrap routes untouched until all callers send a bearer token.
Step 3: Streamlit calls the API with the same identity¶
3a. Identity endpoint¶
Expose GET /me (or similar) using the same Depends(_bearer) as your other protected APIs. Streamlit uses it to drive UI labels without parsing JWTs.
3b. ApiClient with a token from session¶
One call: fluxlit.auth.streamlit.prepare_streamlit_api_client() runs the BFF auth_code exchange when the URL contains it, then returns an ApiClient that already sends Authorization: Bearer … if a token is in st.session_state (default key fluxlit_access_token).
from typing import Any
from fluxlit import prepare_streamlit_api_client
from fluxlit.client import ApiClient
@app.page("/")
def home(st: Any, client: ApiClient) -> None:
_ = client
api = prepare_streamlit_api_client(st)
r = api.get("/me")
if r.status_code == 401:
st.warning("Sign in (e.g. open /api/auth/login).")
return
st.write("Hello,", r.json().get("sub"))
Manual: after login or token exchange, store only a short-lived access token in st.session_state and bind headers per request:
from typing import Any
from fluxlit import bearer_headers_from_session
from fluxlit.client import ApiClient
@app.page("/alt")
def home_alt(st: Any, client: ApiClient) -> None:
_ = client
if "access_token" not in st.session_state:
st.warning("Not signed in.")
return
def hdr() -> dict[str, str]:
return bearer_headers_from_session(st, session_key="access_token")
with ApiClient(auth_header_factory=hdr) as api:
profile = api.get("/me").json()
st.write("Hello,", profile.get("sub"))
3c. Correlation IDs (optional)¶
If you run code in a context where fluxlit.logging.context.set_request_id() is active, enable:
ApiClient(propagate_request_id=True)
so internal calls forward X-Request-ID. In a plain Streamlit script this is usually unset.
Step 4 (optional): OIDC login¶
4a. Discovery and BFF registration¶
Shorter: use fluxlit.app.FluxLit.attach_oidc_login() so public_base_url and first_party_secret default from settings (FLUXLIT_PUBLIC_BASE_URL, FLUXLIT_OIDC_BFF_SECRET):
import os
from fluxlit import FluxLit, GenericOIDCClient, GenericOIDCClientConfig
from fluxlit.config import FluxlitSettings
app = FluxLit(title="OIDC", settings=settings)
oidc = GenericOIDCClient(
GenericOIDCClientConfig(
issuer=os.environ["OIDC_ISSUER"],
client_id=os.environ["OIDC_CLIENT_ID"],
client_secret=os.environ["OIDC_CLIENT_SECRET"],
)
)
oidc.load_discovery_sync()
app.attach_oidc_login(oidc)
Manual: same as above but call fluxlit.auth.oidc.register_oidc_bff_routes() yourself with OIDCBFFConfig if you need full control.
Register the callback URL with your IdP:
{FLUXLIT_PUBLIC_BASE_URL}/api/auth/callback (default callback path; adjust if you change callback_path).
4b. Streamlit: exchange auth_code¶
At the top of your main page:
from typing import Any
from fluxlit import exchange_auth_code_from_query
from fluxlit.client import ApiClient
@app.page("/")
def home(st: Any, client: ApiClient) -> None:
exchange_auth_code_from_query(st, client, exchange_path="/auth/exchange")
...
This performs a server-side POST to /api/auth/exchange and stores fluxlit_access_token. Use that token with ApiClient.for_fluxlit or bearer_headers_from_session when calling APIs that expect your BFF-issued JWT (configure JWTBearer with the same issuer/audience/secret as OIDCBFFConfig).
Step 5: Hardening¶
Environment / settings
export FLUXLIT_ENABLE_SECURITY_HEADERS=1
export FLUXLIT_CORS_ALLOW_ORIGINS='["https://trusted-ui.example.com"]'
export FLUXLIT_PUBLIC_BASE_URL=https://app.example.com
Doctor (CI or pre-deploy)
fluxlit doctor app:app
See Security architecture for threats and token placement, Secrets lifecycle for rotation and avoiding leaks in logs, Production TLS and edge headers for proxies and TLS, and Auth recipes for copy-paste examples (forward-auth, API keys, JWKS).
FluxLit 0.8.1 (auth and internal client)¶
OIDC BFF (custom provider): If you call
fluxlit.auth.oidc.register_oidc_bff_routes()with a customOIDCProvider(anything other thanGenericOIDCClient), setOIDCBFFConfig.allow_unverified_id_token_for_custom_oidc=Trueonly when that provider already verifiedid_tokenor for tests. PreferGenericOIDCClientin production so the BFF validates with JWKS.ApiClient: Paths passed toget/post/requestmust be relative to the API base; absolute or scheme-relative URL strings raiseValueError.public_base_url: Leave unset only for local experiments; production should setFLUXLIT_PUBLIC_BASE_URL(orOIDCBFFConfig.public_base_url) so OAuth redirect URIs are stable behind proxies. An empty value now triggers aUserWarningwhen routes are registered.
FluxLit 0.9.0 (Streamlit page typing)¶
Imports: Streamlit helpers live under
fluxlit.auth.streamlit; the package root re-exportsprepare_streamlit_api_client,bearer_headers_from_session, andexchange_auth_code_from_queryfromfluxlit.Page handlers: Optional
typing.Annotated+Depends,parse_query_params(),PageMetareturns, andfluxlit pages manifest— see Streamlit pages: typing, Depends, and manifests.