For AI agents: the complete documentation index is at llms.txt. Markdown versions are available by appending .md or sending Accept: text/markdown.
Reflex Logo
Docs Logo
Enterprise

/

Auth

/

Secure By Default

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:

auth= valueMeaning
TrueRequire an authenticated user. This is the default for every surface.
FalsePublic. Allow everyone.
a callable checkAn authorization check that runs only after authentication succeeds. A truthy result allows; a falsey result or a raised exception denies.

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_to target 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:

ContextSurfaceExtra attributes
EventAuthContextevent handlerevent_handler (the gated handler), payload (the event payload dict)
VarAuthContextfield / computed varfield_or_var (the Var, or None)
PageAuthContextpage (callable global default only)None

Read the user's claims from ctx.auth_user_state.userinfo, a plain dict:

Expand

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:

SituationOutcome
auth=FalseAllow.
Not logged in (no user resolved)Authentication failure. Handled before any check runs.
Logged in and auth=TrueAllow.
Logged in and the check returns truthyAllow.
Logged in and the check returns falsey or raisesAuthorization failure. Never a login redirect.

What each failure does depends on the surface:

SurfaceAuthentication failure (anonymous)Authorization failure (check said no)
Event handlerblock + redirect to /loginblock + "Action not allowed" toast
Pageredirect to /login (with redirect_to)redirect to /forbidden
Field / computed varwithheld (placeholder / default shown)withheld (placeholder / default shown)

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 reflex or reflex_enterprise.
  • Any OIDCAuthState subclass, 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:

AttributeValue
User.nameThe user's name claim.
User.emailThe user's email claim.
User.subThe user's subject identifier.
User.pictureThe user's picture URL.

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:

CallReturns
await User.current()The current user's OIDCUserInfo claims dict for this event, or None when anonymous.
await User.current_provider()The provider class that actually authenticated this event's user, or None. Correct in multi-provider setups.
User.logoutEvent handler that signs the current user out. Bind it (on_click=User.logout) or return it from a handler. Redirects to the home page (/) when anonymous.

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(...):

  • 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.
Built with Reflex