Auth Lifecycle Hooks
Overview
OpenViper auth lifecycle hooks let applications extend login and logout
behavior without replacing authentication internals. Hooks run around the
built-in authentication flows and receive an AuthHookContext object with
the authenticated user, request metadata, and safe lifecycle data.
Plaintext passwords, OTP codes, API keys, access tokens, and refresh tokens are not passed to hooks by default.
Hook Lifecycle
The built-in login views run hooks in this order:
Credentials are validated by the existing authentication backend.
before_loginhooks run with the authenticated user and sanitized credentials.The session, JWT, or opaque token is created by the existing OpenViper behavior.
on_loginhooks run with the user and session or token metadata.The normal login response is returned.
Logout builds the context before credential revocation, revokes the active
credential, then runs on_logout hooks.
before_login
before_login runs after credentials are authenticated and before final
session or token creation. It can reject login by raising
AuthHookReject.
from openviper.auth.hooks import AuthHookReject, auth_hooks
@auth_hooks.before_login
async def block_suspended_users(context):
if context.user and context.user.is_suspended:
raise AuthHookReject("This account is suspended.")
Unexpected before_login errors fail closed by default. Configure
AUTH_HOOKS["before_login_error"] = "log" only when you deliberately want
to continue after unexpected hook failures.
on_login
on_login runs after successful session or token creation. It is intended
for side effects such as audit logging, last-login updates, notifications, or
session metadata enrichment.
from openviper.auth.hooks import AuthHookReject, auth_hooks
@auth_hooks.on_login
async def audit_login(context):
await AuditLog.objects.create(
user_id=context.user.id,
action="login",
)
By default, on_login errors are logged and login remains successful.
Set AUTH_HOOKS["on_login_error"] = "raise" to fail login when a post-login
hook fails.
on_logout
on_logout runs during logout after the context is built and after the
active credential is revoked. By default, logout completes even if a hook
fails.
from openviper.auth.hooks import auth_hooks
@auth_hooks.on_logout
async def audit_logout(context):
await AuditLog.objects.create(
user_id=context.user.id,
action="logout",
)
Set AUTH_HOOKS["on_logout_error"] = "raise" if logout hook failures should
propagate.
AuthHookContext
Hooks receive an AuthHookContext:
@dataclass(slots=True)
class AuthHookContext:
user: object | None = None
credentials: dict[str, object] = field(default_factory=dict)
request: object | None = None
session: object | None = None
token: object | None = None
auth_backend: str | None = None
metadata: dict[str, object] = field(default_factory=dict)
credentials contains sanitized values such as username, email, provider,
tenant, or login method. Sensitive values are stripped by default.
token contains token metadata for the built-in JWT and opaque-token login
views, not raw bearer tokens.
Registering Hooks
Decorator registration is the primary API:
from openviper.auth.hooks import auth_hooks
@auth_hooks.before_login
def require_known_tenant(context):
if context.credentials.get("tenant") == "disabled":
raise AuthHookReject("Login rejected.")
Explicit registration is also supported:
from openviper.auth.hooks import register_auth_hook
register_auth_hook("before_login", require_known_tenant)
register_auth_hook("on_login", audit_login)
register_auth_hook("on_logout", audit_logout)
Hooks can be synchronous or asynchronous. Hooks run in registration order. Priority ordering is not implemented in v1.
Using Hooks from Apps
A common pattern is to define hooks in auth_hooks.py and import that
module from an explicit app lifecycle hook.
auth_hooks.py:
from openviper.auth.hooks import AuthHookReject, auth_hooks
@auth_hooks.before_login
async def block_suspended_users(context):
if context.user and context.user.is_suspended:
raise AuthHookReject("This account is suspended.")
@auth_hooks.on_login
async def audit_login(context):
await AuditLog.objects.create(
user_id=context.user.id,
action="login",
)
@auth_hooks.on_logout
async def audit_logout(context):
await AuditLog.objects.create(
user_id=context.user.id,
action="logout",
)
lifecycle.py:
def ready() -> None:
from . import auth_hooks
OpenViper does not automatically discover auth_hooks.py in v1. Import the
module from your app startup path so registration is explicit.
Error Handling
Configure hook error behavior with AUTH_HOOKS:
AUTH_HOOKS = {
"before_login_error": "raise",
"on_login_error": "log",
"on_logout_error": "log",
}
Allowed values are "raise" and "log".
before_login rejects should raise AuthHookReject. Unexpected errors
are wrapped in AuthHookExecutionError when the policy is "raise".
Security Notes
Hooks cannot bypass core authentication. They run only after the existing authentication backend accepts credentials.
The default sanitizer removes these credential fields:
password, password_confirm, otp, totp, secret, token,
refresh_token, access_token, and api_key.
Do not log raw request bodies or bearer tokens from hooks. Hook error logs include hook phase and hook name, but not credentials.
Testing Auth Hooks
Use an isolated AuthHookRegistry for unit tests that only exercise
registration and execution:
registry = AuthHookRegistry()
@registry.before_login
async def hook(context):
...
For integration tests that use the global registry, clear it before and after each test:
from openviper.auth.hooks import auth_hooks
auth_hooks.clear()
Limitations
Auth hooks v1 does not include priority ordering, hook groups, per-backend
enablement, admin inspection, or automatic auth_hooks.py discovery.