Hooks¶
App-specific side effects (a welcome email, a trial grant, an audit log) belong in your code,
not forked into the auth flow. AuthHooks registers callbacks that fire at lifecycle points,
uniformly across every path (a password signup and an OAuth signup both run
on_after_register).
from crudauth import CRUDAuth, AuthHooks
async def welcome(user, *, db, context):
await send_welcome_email(user["email"])
auth = CRUDAuth(..., hooks=AuthHooks(on_after_register=welcome))
A hook may be sync or async. user is passed as a plain dict, not your ORM instance, so
your hooks don't couple to the model.
Hooks run after the operation is done (the account created, the session stored, the password
changed), so they can't block or undo it. An exception a hook raises is logged with its
traceback on the crudauth.hooks logger and the request still succeeds. A hook that writes
through db owns that work: commit it, or roll back on its own failure. For work that must not
be lost, enqueue it from the hook rather than doing it inline.
The hooks¶
| Hook | Fires after | Receives |
|---|---|---|
on_after_register |
an account is created | user, db, context |
on_oauth_login |
an OAuth sign-in resolves an active account | user, info, db, created, context |
on_login_failed |
a refused password login | identifier, user, reason, context |
on_lockout |
a password login refused by the lockout | identifier, retry_after, context |
on_after_login |
a successful login | user, request, context |
on_after_logout |
a logout | user, request, context |
on_after_recovery_verified |
recovery-factor verification confirm | user, db, context |
on_after_password_reset |
password reset confirm | user, db, context |
on_after_password_changed |
in-session password change (/change-password) |
user, db, context |
on_after_email_changed |
email change confirm | user, db, context |
on_after_sudo |
a sudo elevation | user, request, context |
on_after_mfa_enabled |
an account turns MFA on | user, db, context |
on_after_mfa_disabled |
an account turns MFA off | user, db, context |
on_after_recovery_code_used |
an MFA recovery code is spent | user, db, context |
All hooks also receive a context keyword.
A hook handed db commits its own writes; crudauth has already committed its own by then. If the
hook raises, crudauth logs it and rolls back whatever the hook left uncommitted, so the session is
usable again for the rest of the request. A hook may also roll back itself. crudauth reloads the
user afterwards if that expired it.
on_oauth_login runs on every OAuth sign-in that reaches an active account, whether it created the
account, linked it by email, or found it by provider id. info is the provider's normalized
profile (OAuthUserInfo: info.username is the GitHub login, info.raw_data the provider's
payload), so the app can keep provider data that changes over time in step, in the same request.
created says whether this sign-in made the account. It runs after the disabled-account check,
before any MFA challenge and before the session exists, so context.session_handle is None. See
OAuth.
on_login_failed gets the identifier as typed (untrusted input: escape it before it reaches a
page), user (the account it named, or None when it named none) and reason
("invalid_credentials" or "inactive"). The response to the caller doesn't say which; the hook
can, for an audit log or an alert. on_lockout gets the identifier and how many seconds the
lockout has left. Both run on every password login (/login, /token, and
auth.authenticate_password), before the refusal is raised.
HookContext¶
context carries ambient request info, so a hook can log or branch without re-deriving it:
| Field | What it is |
|---|---|
ip_address |
Resolved client IP. |
user_agent |
Raw user-agent string. |
transport |
Which transport authenticated ("session", "bearer", ...). |
request |
The FastAPI Request, when available. |
extra |
A dict for flow-specific extras. |
session_handle |
On a session login or logout, the session's public handle, the same id GET /sessions lists. An audit log can name the session that started and the one that ended, and match either to a revocation, without storing a credential. |
Example: an audit log¶
async def audit_login(user, *, request, context):
await write_audit("login", user_id=user["id"], ip=context.ip_address, ua=context.user_agent)
auth = CRUDAuth(..., hooks=AuthHooks(on_after_login=audit_login))