docs/enterprise/auth/overview.md
New in reflex-enterprise v0.9.1.
rxe.AuthPlugin adds OIDC
(OpenID Connect) authentication to Reflex apps. Add the plugin to
rxe.Config(plugins=[...]), set the provider environment variables, and use
rxe.App().
By default, every page, event handler, base field, and computed var requires an
authenticated user. Use auth=False for surfaces that should be public.
The plugin runs the OIDC Authorization Code + PKCE flow against your identity
provider (IdP) and registers /login, /logout, /callback, and /forbidden
routes.
# Requirements
The auth plugin ships with `reflex-enterprise` (v0.9.1+). Your app **must** use `rxe.App()` (not `rx.App()`), and you must configure an OIDC identity provider via environment variables. Using a plain `rx.App()` with the plugin raises a `ConfigError` at startup.
1. Add rxe.AuthPlugin() to rxconfig.py and configure your OIDC provider
through the OIDC_* environment variables:
import os
import reflex as rx
import reflex_enterprise as rxe
os.environ.setdefault("OIDC_ISSUER_URI", "https://your-idp.example.com")
os.environ.setdefault("OIDC_CLIENT_ID", "your-client-id")
os.environ.setdefault("OIDC_CLIENT_SECRET", "your-client-secret") # optional with PKCE
config = rxe.Config(
app_name="my_app",
plugins=[
rxe.AuthPlugin(),
],
)
With the OIDC_* variables set, the app imports and compiles before the IdP is
reachable. OIDC discovery runs only when a user logs in. Placeholder values are
enough for local builds and CI.
2. Use rxe.App() (not rx.App()) in your app module:
import reflex_enterprise as rxe
app = rxe.App()
3. Register the redirect URI with your IdP. Add the plugin's
auth_callback_endpoint (/callback by default) as an allowed redirect URI in
your identity provider's client settings. Register the full URL (scheme,
host, and path) for each environment. A mismatched value produces
redirect_uri_mismatch; see
deploying to production for the exact
callback URL.
You don't need to write a provider class. The plugin uses the built-in
GenericOIDCAuthState, which reads those variables. See
providers for named and multi-provider
setups.
Once active, the plugin protects four kinds of surface by default:
| Surface | Default | How it's withheld | Opt out / gate |
|---|---|---|---|
Pages (@rxe.page / @rx.page / app.add_page) | login required | redirect to /login | auth=False, or @rxe.page(auth=True) to force login |
Event handlers (@rxe.event) | login required | block + redirect/toast | @rxe.event(auth=False) or auth=<check> |
Base fields (rxe.field / plain rx.field) | withheld until login | replaced with its declared default | rxe.field(default, auth=False) or auth=<check> |
Computed vars (@rxe.var) | withheld until login | replaced with its initial_value (dropped if it has none) | @rxe.var(auth=False) or auth=<check> |
auth=True is the default everywhere, so a plain rx.field(...) or bare
@rxe.var is already protected. Set auth=False to make a surface public.
import reflex as rx
import reflex_enterprise as rxe
class DashboardState(rx.State):
# Protected by default; withheld from the client until login.
revenue: rx.Field[float] = rx.field(0.0)
# Public; always sent to the client.
theme: rx.Field[str] = rxe.field("light", auth=False)
@rxe.event # default auth=True: anonymous callers are redirected to /login
async def refresh(self): ...
@rxe.event(auth=False) # public handler anyone may call
def toggle_theme(self):
self.theme = "dark" if self.theme == "light" else "light"
See secure by default for the full
enforcement model, the four auth= wrappers, and authorization check functions.
reflex_enterprise.auth.User is an alias of
reflex_enterprise.auth.AuthUserState. Use its class-level Vars directly in
components. They are populated by the provider that authenticated the user:
import reflex as rx
from reflex_enterprise.auth import User
def profile() -> rx.Component:
return rx.hstack(
rx.avatar(src=User.picture, fallback="U"),
rx.vstack(
rx.heading(User.name),
rx.text(User.email, color_scheme="gray"),
),
)
Inside an event handler, await User.current() returns the user's
OIDCUserInfo claims dict (or None when anonymous):
import reflex as rx
import reflex_enterprise as rxe
from reflex_enterprise.auth import User
class GreetState(rx.State):
@rxe.event # default auth=True
async def greet(self):
user = await User.current() or {}
return rx.toast(f"Hello {user.get('name') or user.get('sub')}!")
See reading the current user
for the full User API, including frontend Vars, current(), and
current_provider().
Sign the user out by linking to /logout or by binding the User.logout event:
import reflex as rx
from reflex_enterprise.auth import User
# Either link to the logout route.
rx.link("Sign out", href="/logout")
# Or bind the logout event to any component.
rx.button("Sign out", on_click=User.logout)
User.logout signs out whichever provider the user logged in with, so it works
with multiple providers. If no one is signed in there's nothing to sign out, so
it just sends them back to the home page (/).
Example header:
import reflex as rx
from reflex_enterprise.auth import User
def header() -> rx.Component:
return rx.cond(
User.sub != "",
rx.hstack(
rx.text(f"Signed in as {User.name}"),
rx.link("Sign out", href="/logout"),
),
rx.link("Log in", href="/login"),
)
/login, with the requested page preserved as a
redirect_to query parameter./login renders a button per configured provider. The visitor clicks one and
is sent to the IdP's authorization endpoint (Authorization Code + PKCE)./callback, which
validates the OAuth state (CSRF), exchanges the code for tokens, and stores
them in secure cookies.redirect_to. Protected fields, vars, pages,
and handlers now resolve against the authenticated user./logout clears tokens, resets the protected surface of every state, and
chains the provider's logout. A CSRF guard blocks cross-site logout requests
(see
secure by default).auth=, authorization checks, and the User facade./login,
/callback, /logout, and /forbidden components.