New in reflex-enterprise v0.9.1.
Secure by Default
Once rxe.AuthPlugin is configured (see the
overview), every non-exempt page, event
handler, base field, and computed var in your app requires a logged-in user
unless you explicitly opt it out.
The auth= value
auth= accepts three kinds of value:
The four wrappers are exported at top level: rxe.page, rxe.event,
rxe.field, and rxe.var. rxe.event, rxe.field, and rxe.var accept all
three values. rxe.page takes a bool only.
The global default
AuthPlugin(auth=...) sets the default applied to every surface that carries no
explicit auth= of its own. It accepts the same three values:
A plain rx.field(...), a bare @rxe.var, a bare @rxe.event, and an
@rxe.page() with no auth= all inherit this global default. An explicit
per-surface rxe.*(auth=...) always overrides it. Changing
AuthPlugin(auth=...) changes the app baseline without editing each field, var,
event, or page.
Gate the whole app behind a role
A callable global default runs on every untagged page load:
Request extra_scopes=["groups"] when checks need the groups claim. See
providers.
Authenticated users who fail the check are sent to /forbidden, not back to
login. Replace that screen with a custom forbidden
page.
Pages
Protect a page with @rxe.page. For pages, auth is a bool only. Callable
checks are not supported as a per-page argument, although the global default may
still be callable.
auth=None(the default): follow the configured global default (AuthPlugin(auth=...)).auth=True: always require an authenticated user, regardless of the global default.auth=False: public page.
A protected page injects a guard as the first on_load event. Anonymous
visitors are redirected to the login endpoint, and the requested page is
preserved as a redirect_to query parameter. The post-login flow returns them
there.
- The post-login
redirect_totarget is validated against the app's origin. Only same-origin absolute paths (e.g./dashboard) or URLs sharing the app's scheme and host are honored. Any cross-origin, scheme-relative (//evil.test), or backslash target falls back to the index page.
Any extra **page_kwargs are forwarded to rx.page: title, image,
description, meta, script_tags, and on_load. When on_load is provided,
the guard is prepended.
Reading the user on page load
Because the guard is prepended, a page's on_load runs after authentication.
await User.current() (or ctx.auth_user_state.userinfo) is non-None there:
User.current() is event-only; an on_load handler is an event and resolves
the authenticated user from the guard. See reading the current
user.
Pages Added Without @rxe.page
With the plugin active, rxe.App() defaults every page to login-required,
including pages added via app.add_page(...) or a plain @rx.page. Opt out with
auth=False:
Because plain @rx.page takes no auth argument, use
@rxe.page(auth=False) to opt a decorated page out.
When the global default is a callable
A page cannot take a callable auth= directly. If AuthPlugin(auth=...) is
callable, untagged pages (@rxe.page() / @rx.page / app.add_page) run it on
load. An authenticated visitor who fails the check is redirected to the
forbidden_endpoint (/forbidden by default). An explicit
@rxe.page(auth=True) only requires login and does not run the callable
default.
Event handlers
Protect an event handler with @rxe.event. Here auth accepts a bool or a
callable check.
Works bare (@rxe.event) or called (@rxe.event(auth=...)), and can wrap a raw
function or an already-converted EventHandler. Extra **event_kwargs are
forwarded to rx.event: background, stop_propagation, prevent_default,
throttle, debounce, and temporal.
A failed authorization check on an event handler shows the
"Action not allowed" toast (see
authentication vs authorization).
Base fields
Base (state) fields are protected by default. A plain rx.field(...), or a bare
annotation, on one of your state classes is protected and dropped from the state
delta until the user is resolved. Use rxe.field to opt a field out or attach a
check.
A withheld field is replaced in the delta with its declared default (the value baked into the frontend bundle) until the user is resolved.
Computed vars
Computed vars are protected by default and withheld until login. Wrap them with
@rxe.var.
Usable bare (@rxe.var) or called (@rxe.var(auth=..., initial_value=...)).
Extra **var_kwargs are forwarded verbatim to rx.var: initial_value,
cache, deps, auto_deps, interval, and backend.
Authorization checks
For finer-grained control than "any authenticated user," attach a callable
auth= check to the specific events, vars, or fields that need it. A check is a
function that receives a single context object and returns a bool (or an
awaitable of one):
A check runs only after authentication succeeds. Anonymous callers are redirected to login before the check runs, and a resolved user is always present inside the check.
The context object
Each surface passes a different context, all carrying the current user as
ctx.auth_user_state (an AuthUserState). Import them from
reflex_enterprise.auth:
Read the user's claims from ctx.auth_user_state.userinfo, a plain dict:
ExpandCollapse
One check for any surface
Annotate ctx with the AuthContext union to make one function usable on any
surface; annotate it with a single context type to restrict it (and get exact
autocomplete). isinstance(ctx, EventAuthContext) narrows the union inside the
body:
Async checks
Most checks can be sync. Read OIDC claims from
ctx.auth_user_state.userinfo:
Request extra_scopes=["groups"] during OAuth login when checks depend on the
groups claim.
A check may also be async. The framework awaits it at the right point: the per-event gate for handlers, or delta resolution for fields and vars. Use an async check when it calls async APIs, for example:
- Querying a database.
- Calling a remote authorization service, such as OpenFGA or another ReBAC backend.
- Accessing another Reflex state with
await ctx.auth_user_state.get_state(...)when the policy input is stored in state.
Authentication vs authorization
The two failure modes are deliberately different. Applied per surface against the resolved user:
What each failure does depends on the surface:
Two properties follow from the ordering: a check never runs for an anonymous caller, and a check that raises fails closed. Exceptions are treated as deny results.
How withholding works
Protected base fields are dropped from the state delta, and protected computed vars are withheld, for any caller who isn't authorized to see them. A sync check is evaluated inline as the delta is built; an async check is deferred and awaited during delta resolution.
The hydrate event runs before the auth cookies are known. Even for a
logged-in user, protected values are withheld at first because the user has not
been resolved yet. Once an event resolves an authenticated user (for example,
the page guard on a protected page), the protected names are re-delivered in
that event's delta, filtered against the resolved user.
Set initial_value on protected computed vars. The placeholder is baked into
the frontend bundle and shown until the real value arrives after login.
Logout resets protected state
On logout, each non-exempt state's protected surface is reset. This prevents one user's session data from leaking to the next user on the same client token:
- Protected base vars revert to their declared defaults.
- Protected cached computed vars are dropped.
- Server-only backend vars are cleared.
Public (auth=False) fields and vars are preserved across logout. They are
not part of the authenticated session.
Logout is protected against CSRF
The plugin installs middleware for the configured logout_endpoint. Cross-site
GET navigations to /logout are blocked when the browser sends
Sec-Fetch-Site: cross-site; those requests are redirected to the frontend root.
Same-origin logout requests continue normally. No configuration is required.
Exempt states
Some state classes are never protected and never gated:
- State classes defined inside
reflexorreflex_enterprise. - Any
OIDCAuthStatesubclass, including user-defined auth providers.
These exemptions let provider states read auth cookies before login and let the page guard resolve the current user without being gated.
Reading the current user
Import the User facade to read the current user from either the frontend or the
backend:
User is an alias of reflex_enterprise.auth.AuthUserState and may be used
interchangeably.
Frontend Vars: embed these class-level descriptors directly in components.
They bind to AuthUserState, populated after login by the provider that
authenticated the user. Each is typed str and is empty ("") until login:
Reading other claims in a component
The common OIDC claims name, email, sub, and picture are projected as
frontend Vars. The full userinfo dict is server-only and never serialized. To
render any other claim (groups, roles, a custom field) in a component, opt it
onto the frontend by declaring a computed var on an AuthUserState substate:
For backend checks, read the same claims via await User.current() or
ctx.auth_user_state.userinfo.get(...). See
the claims a provider
returns.
Backend: use these inside an event handler. current() and
current_provider() are async:
Inside an authorization check, ctx.auth_user_state.provider returns the same
provider class.
OIDCUserInfo is a plain dict at runtime. Read claims with .get(...):
Related
- Overview: plugin setup and the login flow.
- Providers: provider configuration.
- Custom pages: custom login, callback, logout, and forbidden pages.
- Testing: unit tests and mock-IdP flow tests.