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

/

Audit

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:

ActionEmitted whenOutcomes
login_startedThe authorization redirect to the IdP is issued.success / failure
login_completedThe callback token exchange finishes (or fails).success / failure
logoutThe session is cleared — user logout, popup sync, or stale-identity cleanup.success / failure
token_refreshAn access-token refresh exchange completes (or is rejected).success / failure
session_expiredA session dies without a logout: invalid tokens, rejected userinfo, or cookies that expired out-of-band.success

Access decisions record every allow/deny the plugin makes, phrased as what the user experienced:

ActionSurfaceOutcomes
event_handlerThe per-event gate on protected event handlers.allowed / redirected_to_login / denied_toast
page_loadThe page guard on protected pages.allowed / redirected_to_login / redirected_to_forbidden

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.

FieldMeaning
stateThe state instance the audited flow ran on — always present, and the door into app state via await context.state.get_state(...).
auth_user_stateThe live per-token AuthUserState handle, or None if it could not be loaded.
userinfoA claims snapshot at emission time. Emissions adjacent to a session reset (logout, expiry) capture it before the reset, so the event still records who logged out.
providerThe involved provider's name, when known.
routeThe current page URL with query and fragment stripped (the query can carry OAuth code/invite tokens), when available.
session_idThe router session id, when available.
handler_nameThe gated handler's function name (event_handler action only).
event_handlerThe gated EventHandler object (event_handler action only).
payloadThe gated event's payload, verbatim (event_handler action only).
reasonMachine-readable cause, e.g. "refresh_failed".
error_txidCorrelates with the provider's backend error logs.
timestamptime.time() at emission.

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

Built with Reflex