Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,31 @@ def get_sidebar_items_enterprise_usage():
),
],
),
SideBarItem(
names="Authentication",
children=[
SideBarItem(
names="Overview",
link=enterprise.auth.overview.path,
),
SideBarItem(
names="Secure by Default",
link=enterprise.auth.secure_by_default.path,
),
SideBarItem(
names="OIDC Providers",
link=enterprise.auth.providers.path,
),
SideBarItem(
names="Customizing the Auth Pages",
link=enterprise.auth.custom_pages.path,
),
SideBarItem(
names="Testing Guarded Code",
link=enterprise.auth.testing.path,
),
],
),
]


Expand Down
5 changes: 4 additions & 1 deletion docs/app/reflex_docs/whitelist.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
"""

WHITELISTED_PAGES = [
# "/getting-started/introduction",
# Auth docs preview — matches all 5 pages under /enterprise/auth/ by prefix,
# plus the enterprise overview landing page so navigation into the section works.
"/enterprise/auth",
"/enterprise/overview",
]
Comment on lines 13 to 18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Non-empty whitelist breaks all non-auth docs pages

_check_whitelisted_path is called unconditionally during page registration in __init__.py. When WHITELISTED_PAGES is non-empty, every page whose route does not start with one of the listed prefixes returns False from resolve_doc_route and is skipped entirely. Merging this change means any build environment that imports reflex_docs.pages.docs — including the production build — would compile only the six auth/enterprise-overview pages and silently drop the entire rest of the documentation site. The original file intentionally used an empty list to build everything.



Expand Down
165 changes: 165 additions & 0 deletions docs/enterprise/auth/custom-pages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
---
title: Customizing the Auth Pages
---

_New in reflex-enterprise v0.9.1._

# Customizing the Auth Pages

`rxe.AuthPlugin` registers three friendly routes and owns their **wiring**:

| Endpoint | Default route | Plugin-owned wiring |
| --- | --- | --- |
| `login_endpoint` | `/login` | Renders the login palette / starts the OIDC redirect. |
| `auth_callback_endpoint` | `/callback` | CSRF (OAuth `state`) check + authorization-code token exchange, then redirect back. |
| `logout_endpoint` | `/logout` | Dispatches the active provider's logout. |

You can replace only the **rendered component** on each route via the
`login_page`, `callback_page`, and `logout_page` builders — the `on_load`
wiring (login redirect, callback token exchange, logout dispatch) stays
plugin-owned, so the real OIDC flow is never something you reimplement.

The routes themselves are configurable through `login_endpoint`,
`logout_endpoint`, and `auth_callback_endpoint` (defaults `/login`, `/logout`,
`/callback`). See the [providers](/docs/enterprise/auth/providers/) page for
configuring identity providers, and the
[overview](/docs/enterprise/auth/overview/) for how the plugin fits together.

```md alert warning
# Register the callback URI with your IdP
If you change `auth_callback_endpoint`, register that exact URI as the OAuth redirect URI with your identity provider, or the token exchange will be rejected.
```

## The page builder contract

A page builder is a callable that receives the build context as **keyword
arguments**:

| Keyword | Type | Meaning |
| --- | --- | --- |
| `providers` | `Sequence[type[OIDCAuthState]]` | The resolved provider state classes. |
| `plugin` | `AuthPlugin` | The plugin instance. |

Name the entries you need and add `**context` to ignore the rest:

```python
import reflex as rx


def custom_login_page(providers, **context) -> rx.Component: ...
```

A builder may also take all of it with `**context` only. The same contract
applies to the login, callback, and logout builders.

## A custom login page

The login builder wraps each provider's `get_login_button(*children)` so the
real OIDC redirect wiring is unchanged — only the surrounding layout is yours.
Loop over `providers` and pass the clickable element you want as the button's
children. `provider.display_name()` returns a pretty name (it defaults to the
provider's `__provider__` title-cased):

```python
import reflex as rx


def custom_login_page(providers, **context) -> rx.Component:
return rx.center(
rx.vstack(
*[
provider.get_login_button(
rx.button(f"Continue with {provider.display_name()}")
)
for provider in providers
],
),
)
```

With two or more providers this naturally renders one button per provider — a
login palette where the visitor picks an identity provider.

## Custom callback and logout pages

The callback and logout routes only show an interstitial while their
plugin-owned `on_load` runs. Reuse `providers[0].get_authentication_loading_page()`,
which already shows the validating and redirecting states as the exchange (or
logout) proceeds — and an error view if it fails:

```python
import reflex as rx


def custom_callback_page(providers, **context) -> rx.Component:
return providers[0].get_authentication_loading_page()


def custom_logout_page(providers, **context) -> rx.Component:
return providers[0].get_authentication_loading_page()
```

Wrap that view in your own layout to brand the interstitial — for example a
centered card with a heading above the loading view.

## Wiring them up

Pass the builders to the plugin in `rxconfig.py` as **import-path strings**
(`"module.function"`). The builder modules import `reflex_enterprise`, which
loads `rxconfig` at import time, so importing them in `rxconfig.py` would
re-enter the config; the plugin resolves the strings lazily at compile time
instead:

```python
import reflex as rx

import reflex_enterprise as rxe

config = rxe.Config(
app_name="my_app",
plugins=[
rxe.AuthPlugin(
login_page="my_app.auth_pages.custom_login_page",
callback_page="my_app.auth_pages.custom_callback_page",
logout_page="my_app.auth_pages.custom_logout_page",
),
],
)
```

```md alert info
# Strings in rxconfig, callables elsewhere
The import-path string is only required because of the rxconfig re-entry. If you build the `AuthPlugin` somewhere the builder is already importable, you can pass the callable directly: `login_page=custom_login_page`.
```

## Defaults

Omit a builder and the plugin falls back to its defaults from
`reflex_enterprise.auth.pages`:

| Builder argument | Default | Renders |
| --- | --- | --- |
| `login_page` | `default_login_page` | One `provider.get_login_button()` per provider. |
| `callback_page` | `default_callback_page` | `providers[0].get_authentication_loading_page()`. |
| `logout_page` | `default_logout_page` | `providers[0].get_authentication_loading_page()`. |

The defaults take the same `providers` / `plugin` keyword context, so a custom
builder may call one to wrap the default content in its own layout:

```python
import reflex as rx

from reflex_enterprise.auth.pages import default_login_page


def custom_login_page(providers, **context) -> rx.Component:
return rx.center(default_login_page(providers=providers, **context))
```

## Related

- [Providers](/docs/enterprise/auth/providers/) — configure the identity
providers the login page renders buttons for.
- [Testing](/docs/enterprise/auth/testing/) — verify guarded surfaces.
- [Secure by default](/docs/enterprise/auth/secure-by-default/) — how the rest
of the app is protected.
117 changes: 117 additions & 0 deletions docs/enterprise/auth/overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
---
title: Authentication Overview
---

_New in reflex-enterprise v0.9.1._

# Authentication Overview

`rxe.AuthPlugin` adds OIDC (OpenID Connect) authentication to your Reflex app
with a **secure-by-default** model. Once the plugin is in
`rxe.Config(plugins=[...])`, four surfaces require a logged-in user unless you
explicitly opt out: **pages** (anonymous visitors are redirected to login),
**event handlers** (anonymous callers are blocked and redirected), **base state
fields** (dropped from the state delta until login), and **computed vars**
(withheld until login). The plugin runs the real OIDC Authorization Code + PKCE
flow against your identity provider and auto-registers friendly `/login`,
`/logout`, and `/callback` routes.

```md alert warning
# Requirements
Requires `reflex-enterprise` with the auth plugin (v0.9.1+). Your app must use `rxe.App()` (not `rx.App()`), and you must configure an OIDC identity provider via environment variables.
```

## Quickstart

Add `rxe.AuthPlugin()` to the `plugins` list of `rxe.Config` in `rxconfig.py`,
and configure your OIDC provider through the `OIDC_*` environment variables:

```python
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")

config = rxe.Config(
app_name="my_app",
plugins=[
rxe.AuthPlugin(),
],
)
```

Your app must use `rxe.App()` (not `rx.App()`):

```python
import reflex_enterprise as rxe

app = rxe.App()
```

With the `OIDC_*` variables set you need **no custom provider** — the plugin
defaults `auth_providers` to `[GenericOIDCAuthState]`, which reads
`OIDC_ISSUER_URI`, `OIDC_CLIENT_ID`, and `OIDC_CLIENT_SECRET`. Register the
plugin's `auth_callback_endpoint` (`/callback` by default) as the redirect URI
with your IdP. See [providers](/docs/enterprise/auth/providers/) for named and
multi-provider setups.

## The four protected surfaces

Each surface is protected by default and has its own way to opt out or gate:

| Surface | Default | Opt out / gate |
| --- | --- | --- |
| Pages (`@rxe.page` / `app.add_page` / `@rx.page`) | login required | `@rxe.page(auth=False)` or `app.add_page(..., auth=False)` |
| Event handlers (`@rxe.event`) | login required | `@rxe.event(auth=False)` or `@rxe.event(auth=<check>)` |
| Base fields (`rxe.field` / plain `rx.field`) | withheld until login | `rxe.field(default, auth=False)` |
| Computed vars (`@rxe.var`) | withheld until login | `@rxe.var(auth=False)` |

`auth=True` is the secure default on every surface, so a plain `rx.field(...)`
or a bare `@rxe.var` on a non-exempt state is already protected. Pass
`auth=False` to opt a surface out and make it public. Event handlers and
fields/vars also accept a **callable** authorization check that runs only after
authentication succeeds; pages take `auth` as a bool only. See
[secure-by-default](/docs/enterprise/auth/secure-by-default/) for the full
enforcement model and check-function signatures.

## Reading the current user

`reflex_enterprise.auth.User` is the app-facing handle on the current user. Its
class-level Vars — `User.name`, `User.email`, `User.sub`, `User.picture` — embed
directly in components (`rx.text(User.name)`, `rx.avatar(src=User.picture)`).
Inside an event handler, `await User.current()` returns the user's `OIDCUserInfo`
dict (or `None` when anonymous):

```python
import reflex as rx
import reflex_enterprise as rxe
from reflex_enterprise.auth import User


class DemoState(rx.State):
@rxe.event # default auth=True
async def protected_action(self):
user = await User.current() or {}
return rx.toast(f"Hello {user.get('name') or user.get('sub')}!")
```

See [secure-by-default](/docs/enterprise/auth/secure-by-default/) for the full
`User` facade and how protected values are delivered after login.

## Learn more

- [Secure by default](/docs/enterprise/auth/secure-by-default/) — the
enforcement model, the four `auth=` wrappers, check functions, and the `User`
facade.
- [Providers](/docs/enterprise/auth/providers/) — `GenericOIDCAuthState`, named
and multi-provider setups, and OIDC environment variables.
- [Custom pages](/docs/enterprise/auth/custom-pages/) — replacing the rendered
`/login`, `/callback`, and `/logout` components with your own builders.
- [Testing](/docs/enterprise/auth/testing/) — exercising guarded surfaces with
`auth_as`.
- [Enterprise overview](/docs/enterprise/overview/) — the rest of
reflex-enterprise.
Loading
Loading