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

/

Providers

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__.

KeyProvider-specificShared fallbackNotes
Issuer{PROVIDER}_ISSUER_URIOIDC_ISSUER_URIThe IdP issuer URL (its .well-known/openid-configuration is discovered from here).
Client ID{PROVIDER}_CLIENT_IDOIDC_CLIENT_IDThe OAuth client id.
Client Secret{PROVIDER}_CLIENT_SECRETOIDC_CLIENT_SECRETOptional; PKCE works without it.

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 as offline_access, do not trigger invalid_scope during refresh.
  • Without offline_access there 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 against ctx.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:

HookPurpose
_validate_tokens(self) -> boolValidate the current access and ID tokens; return whether they're valid.
_verify_jwt(self, token_json) -> TokenVerify the ID token JWT; override to customize verification.
_valid_issuers(self) -> list[str] | NoneAcceptable iss claim values; override for e.g. Azure multi-tenant.
_set_tokens(self, access_token, id_token=None, refresh_token=None, granted_scopes=None, **kwargs)Persist tokens after exchange; override to handle extra response data.
_set_tokens_payload_from_exchange(self, exchange) -> dictBuild the kwargs passed to _set_tokens; override to forward an extra field from the token-exchange response.
_validate_auth_callback_exchange(self, exchange) -> dict | NoneValidate the token-exchange response from the callback.
_fetch_userinfo(self) -> OIDCUserInfoFetch claims from the IdP's userinfo endpoint; override to fetch or reshape claims.
_redirect_to_login_payload(self) -> dictBuild the authorization-request query params (scope, state, PKCE challenge); override for non-standard login params.
_redirect_to_logout_payload(self) -> dict[str, str]Build the IdP end-session params (state, id_token_hint, post_logout_redirect_uri); override for a custom post_logout_redirect_uri or non-standard end-session.
_on_access_token_change(self, new_access_token, refresh=False)React when the access token is set or refreshed.
_on_refresh_access_token(self, new_access_token)React specifically when the access token is refreshed.

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.

Expand

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.

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