New in reflex-enterprise v0.9.3.
Auditing Auth Actions
rxe.AuthPlugin(audit=...) registers a single observe-only hook that is called
for every auth lifecycle action (login, logout, token refresh, session expiry)
and every access decision the plugin makes on your behalf (the per-event gate
and the page guard). Use it to feed a SIEM, an audit table, or a structured
log — anywhere "who did what, and was it allowed" needs to be recorded.
Like auth_providers and the page builders, audit= accepts either the
callable itself or a "module.function" import-path string. Use the string
form in rxconfig.py — app modules cannot be imported while the config is
still being assigned. The path is resolved at compile time, so a typo fails
startup with a clear error rather than surfacing at the first audited event.
The hook contract
The hook is called as audit(action, outcome, context):
- Sync or async — both are accepted; an awaitable result is awaited.
- Observe-only — the return value is ignored. The hook can never veto or
alter a decision; authorization semantics stay in the
auth=checks. - Fail-open — a raising hook is logged at
ERROR(with traceback) and the auth flow proceeds unaffected. - Awaited inline — events for a session arrive in causal order, but the hook runs on the hot path. Keep it fast; heavy sinks (HTTP, database) should enqueue internally and flush out-of-band, projecting the context down to the plain fields they need first.
AuditAction and AuditOutcome are str-valued enums, so action.value and
outcome.value drop into any log or JSON record as plain strings. The context
does not serialize as-is: state, auth_user_state, and event_handler are
live framework objects and payload is arbitrary event input, so handing the
whole AuditContext to a standard JSON encoder raises TypeError. Build the
record from the plain fields the sink needs (handler_name, provider,
route, reason, ...). Always read .value when formatting: on Python 3.11+
f"{action}" renders AuditAction.LOGIN_COMPLETED while 3.10 renders the
bare value.
Actions and outcomes
Lifecycle actions record the user interacting with the plugin:
Access decisions record every allow/deny the plugin makes, phrased as what the user experienced:
Public surfaces are not audited: an explicit auth=False handler, a public
page under an auth=False default, and framework events like hydrate emit
nothing — auditing "everything is public and allowed" would flood the sink.
Field and computed-var withholding (delta filtering and redelivery) is also
not audited; those derive from the same identity the gate and guard decisions
already recorded.
On failures, context.reason carries a machine-readable cause — for example
"csrf_state_mismatch", "token_exchange_failed", "refresh_failed",
"tokens_invalid", or "stale_cookies" — and context.error_txid matches
the error transaction id in the provider's backend logs and error UI.
The context
AuditContext is frozen and keyword-only; new optional fields may be added
over time, but the 3-argument hook signature never changes.
Writing audit events into app state
context.state works exactly like self in an event handler: reach any
state via get_state. This trail state is deliberately public (auth=False)
so denial events recorded while anonymous still render after the redirect:
Entries ride along with the next state delta, so a decision made while a redirect is in flight (a gate-blocked event, for example) shows up once the visitor lands back on a page.
Volume
event_handler/allowed is the highest-frequency emission: it fires once per
protected handler per event, so a busy app records a lot of "allowed". Hooks
that only care about denials or lifecycle events should filter on
action/outcome first, before doing any I/O.
Popup flows
Popup login/logout emits for both windows' sessions: the popup window's own
session records the provider round-trip (login_started, login_completed),
and the opener's session records the token handoff — login_completed with
reason "popup_tokens_synced", logout with reason "popup_logout_synced".
Hooks counting unique logins should key on one side of the pair; identity
(userinfo) is most complete on the opener's events.
See the overview for how the plugin fits
together, and secure by default
for the auth= checks whose decisions these events record.