New in reflex-enterprise v0.9.1.
OIDC Providers
An OIDC provider is the state class that runs the OpenID Connect Authorization
Code + PKCE flow against your identity provider (IdP). rxe.AuthPlugin ships a
built-in provider that resolves its configuration from environment variables.
See secure by default for how the plugin protects pages, events, fields, and computed vars, and the overview for setup.
The default provider
GenericOIDCAuthState is the built-in provider. It reads three environment
variables:
AuthPlugin.auth_providers defaults to [GenericOIDCAuthState]. With the
OIDC_* variables set, no provider class is required:
Register the plugin's auth_callback_endpoint (default /callback) as the
redirect URI with your IdP.
Naming a provider
To use provider-specific environment variables (and to register multiple distinct
IdPs), subclass OIDCAuthState and set __provider__. The subclass must also
inherit rx.State:
With __provider__ = "okta", config resolution prefers the
OKTA_ISSUER_URI / OKTA_CLIENT_ID / OKTA_CLIENT_SECRET variables, falling
back to the shared OIDC_* keys:
After the provider is registered, the default /login page shows a login button
for it. display_name() controls the button label. By default, it returns the
title-cased __provider__ value ("okta" -> "Okta"):
Running inside an iframe
Embedded apps use a popup login/logout flow instead of top-level redirects. The
popup is opened from the user's click and posts tokens back to the app's own
origin with postMessage.
When customizing /login, call provider.get_login_button(*children) for each
provider. This mounts the message listener used by the popup flow. Calling
redirect_to_login directly does not mount that listener.
In most apps, link users to /login and customize the login page when the
default layout is not enough. See
custom pages.
Override _use_popup_flow(self) -> bool on the provider subclass to force or
disable the popup flow. The default returns whether the app is embedded.
Environment variables
Each provider resolves every config key by trying the provider-specific
{PROVIDER}_{KEY} variable first, then falling back to the shared OIDC_{KEY}.
{PROVIDER} is the uppercased __provider__.
The default GenericOIDCAuthState (__provider__ = "generic") resolves
GENERIC_* then OIDC_*. For the default provider, set only the OIDC_* keys.
Registering providers with the plugin
AuthPlugin(auth_providers=[...]) accepts provider classes or
"module.ClassName" import-path strings. Strings are resolved lazily at
compile time. Order is preserved, and the two forms may be mixed. The default is
[GenericOIDCAuthState].
In rxconfig.py, pass strings:
Outside rxconfig.py (for example, in tests) you may pass the classes
themselves:
Scopes and refresh tokens
extra_scopes is forwarded to every configured provider and merged into the
scopes each one requests. The merge is deduped and preserves the existing scopes
(openid email profile by default):
extra_scopes=["offline_access"]asks the IdP to issue a refresh token. Once granted, the framework refreshes the access token as it nears expiry, coordinated across browser tabs. Refresh requests include only scopes the IdP originally granted; scopes consumed but not granted by the IdP, such asoffline_access, do not triggerinvalid_scopeduring refresh.- Without
offline_accessthere is no refresh token. The session ends when the access token expires, and the user is returned to/login. - If a refresh fails (the refresh token was revoked or expired), the session is reset and the user is logged out.
extra_scopes=["groups"]requests group claims, useful for authorization checks againstctx.auth_user_state.userinfo.get("groups").
Independent of token expiry, every auth cookie has a fixed 7-day lifetime. This is the effective maximum session length. See the deployment guide for the HTTPS and cookie requirements.
Per-provider scopes
extra_scopes is applied uniformly to every configured provider and only
adds to the defaults. To give a single provider a distinct set, set the
_requested_scopes class attribute on that subclass. Unlike extra_scopes,
this replaces the default "openid email profile" rather than merging into
it.
Reading granted scopes
Every provider exposes a granted_scopes Var holding the space-delimited scopes
the IdP actually granted. await User.current_provider() resolves whichever
provider authenticated the current user, so derive a computed var on your own
state that reads the active provider's scopes — then rx.cond gates on it,
regardless of which provider the user logged in with:
Granted scopes are fixed once the user logs in, so the var carries no reactive
dependencies (auto_deps=False, deps=[]): it resolves on login and stays stable
for the session.
Multiple providers
/login shows one login button per provider, including when there is only one —
it never redirects to an IdP automatically. The callback resolves the
initiating provider from the OAuth state parameter; logout resolves the
active provider currently holding tokens.
Reading the user is provider-agnostic: User.name / .email / .sub /
.picture bind to AuthUserState, which is populated by whichever provider
completes login. To branch on the provider in backend code, use
await User.current_provider():
The claims a provider returns
OIDCUserInfo is a TypedDict (total=False) declaring only sub. The OIDC
spec requires that claim. Profile claims such as name, email, and picture
appear only when their scope is granted. At runtime, OIDCUserInfo is a plain
dict; read claims with .get(...).
To document the extra claims a provider returns, declare a nested
UserInfo(OIDCUserInfo, total=False) on your provider:
The common claims are projected as read-only Vars on User
(User.name, .email, .sub, .picture); any other claim is read from the
dict via await User.current() or ctx.auth_user_state.userinfo.get(...).
Advanced extension points
OIDCAuthState exposes overridable async hooks for provider-specific behavior.
Override only the hooks required by your provider subclass:
Using the access token to call an API
Inside an @rx.event (or computed var) on your OIDCAuthState subclass,
await self._access_token to get the current OAuth access token, then send it as
a bearer token to a downstream service or the IdP. The leading underscore does
not indicate direct field access here: _access_token is a server-only
awaitable Var, never exposed to the browser, and kept fresh by background
refresh.
To react when the token is issued or refreshed, override
_on_access_token_change or _on_refresh_access_token on the subclass.
Sharing behavior across providers
To put the same fields, vars, event handlers, or hook overrides on more than one
provider without duplication, define an rx.State mixin with mixin=True and
list it before OIDCAuthState in each provider's bases:
Base order matters: the mixin must come before OIDCAuthState/rx.State. Each
provider reads its own __provider__-namespaced cookies. The shared code
operates on that provider's tokens.
Persisting extra token-exchange data
_set_tokens_payload_from_exchange builds the payload (access_token, plus any
of id_token, refresh_token, and granted_scopes the exchange returned) from
the IdP's token-exchange response, and that payload is the entire set of kwargs
_set_tokens receives on both the callback and refresh paths.
To persist a custom field from the exchange, override both hooks:
_set_tokens_payload_from_exchange carries the field through, and _set_tokens
accepts and stores it.
ExpandCollapse
Under the popup/iframe flow, the popup and opener are separate clients. The popup
runs the callback and captures the field in its own state, then posts the tokens
to the opener, which applies them through on_iframe_auth_success, not
_set_tokens_payload_from_exchange. Only the posted tokens cross over, so a
custom field stored on the popup's self does not automatically reach the opener.
Give the extra _set_tokens keyword a default (as org_id="" above) so the
opener's call works.
Migrating from register_auth_endpoints
OIDCAuthState.register_auth_endpoints(app) is deprecated (since
reflex-enterprise v0.9.1, removed in 1.0). Register rxe.AuthPlugin in
rxe.Config(plugins=[...]) instead. The plugin registers /login, /logout,
/callback, and /forbidden, and applies the secure-by-default protections.
Related
- Secure by default: protected surfaces and authorization checks.
- Custom pages: custom login, callback, logout, and forbidden page builders.
- Testing: guarded surfaces and mock-IdP tests.