[PR #8915] feat: add lifecycle event callbacks for security-sensitive operations #16541

Open
opened 2026-04-13 10:34:00 -05:00 by GiteaMirror · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/better-auth/better-auth/pull/8915
Author: @nphlp
Created: 4/2/2026
Status: 🔄 Open

Base: nextHead: feat/lifecycle-hooks


📝 Commits (4)

  • d0f00a4 feat: add lifecycle event callbacks for security-sensitive operations
  • 6959690 fix: wrap onMagicLinkRequested with runInBackgroundOrAwait
  • 5396115 fix: separate findSession/deleteSession error handling in sign-out
  • 8e4dd38 test: add onPasskeyAdded and onPasskeyDeleted callback tests

📊 Changes

19 files changed (+552 additions, -44 deletions)

View changed files

📝 .gitignore (+5 -0)
📝 docs/content/docs/concepts/hooks.mdx (+168 -0)
📝 packages/better-auth/src/api/routes/email-verification.test.ts (+27 -0)
📝 packages/better-auth/src/api/routes/email-verification.ts (+8 -0)
📝 packages/better-auth/src/api/routes/password.test.ts (+15 -0)
📝 packages/better-auth/src/api/routes/password.ts (+8 -0)
📝 packages/better-auth/src/api/routes/sign-in.ts (+12 -0)
📝 packages/better-auth/src/api/routes/sign-out.test.ts (+13 -0)
📝 packages/better-auth/src/api/routes/sign-out.ts (+19 -0)
📝 packages/better-auth/src/api/routes/update-user.ts (+9 -0)
📝 packages/better-auth/src/plugins/magic-link/index.ts (+14 -0)
📝 packages/better-auth/src/plugins/magic-link/magic-link.test.ts (+24 -0)
📝 packages/better-auth/src/plugins/two-factor/index.ts (+19 -0)
📝 packages/better-auth/src/plugins/two-factor/types.ts (+16 -0)
📝 packages/core/src/types/init-options.ts (+40 -1)
📝 packages/passkey/src/index.ts (+1 -1)
📝 packages/passkey/src/passkey.test.ts (+75 -0)
📝 packages/passkey/src/routes.ts (+63 -42)
📝 packages/passkey/src/types.ts (+16 -0)

📄 Description

Summary

Better Auth has generic before/after hooks (middleware that intercepts all endpoints), but there are no purpose-built callbacks for reacting to specific security events (login, logout, password change, etc.).

This PR adds lifecycle callbacks — optional callbacks configured directly in the relevant options section, triggered after security-sensitive operations. They enable logging, analytics, security notifications, or any side-effect without writing a plugin or an after hook with ctx.path matching.

Added callbacks

Core options:

  • onLogin — triggered after a session is created (email/password, magic link, OAuth, passkey)
  • onLogout — triggered after a session is deleted

emailAndPassword options:

  • onPasswordChanged — triggered when a user changes their password from their profile
  • onResetPasswordRequested — triggered when a password reset is requested (runs alongside sendResetPassword)

emailVerification options:

  • onEmailVerificationRequested — triggered when a verification email is sent (runs alongside sendVerificationEmail)

Two-Factor plugin:

  • onTotpEnabled / onTotpDisabled — triggered when 2FA is enabled/disabled

Passkey plugin:

  • onPasskeyAdded / onPasskeyDeleted — triggered when a passkey is added/deleted

Magic Link plugin:

  • onMagicLinkRequested — triggered when a magic link is sent (runs alongside sendMagicLink)

Use cases

  • Security notifications: send an alert email when a user logs in from a new device (onLogin), changes their password (onPasswordChanged), or enables/disables 2FA (onTotpEnabled/onTotpDisabled)
  • Audit / logging: log security-sensitive events (logins, password resets, passkey additions/deletions) to an external system
  • Analytics: track authentication events (email verification rate, 2FA adoption, magic link usage)
  • Abuse detection: detect suspicious patterns (multiple password reset requests via onResetPasswordRequested)

Technical decisions

  • runInBackgroundOrAwait: all callbacks use this existing pattern — they execute synchronously by default, or are deferred when a backgroundTasks handler is configured.
  • Observational callbacks: the onXxxRequested callbacks run alongside functional callbacks (sendResetPassword, sendVerificationEmail, sendMagicLink), they don't replace them. This separates email delivery (functional) from logging/analytics (observational).
  • Consistent signature: (data: { ... }, request?: Request) => Promise<void> for all core callbacks. Plugins follow their own context signature (GenericEndpointContext for magic-link).
  • JSDoc on every callback with clear description.

Fix

  • onPasswordReset: JSDoc said "when a user's password is changed successfully" but it fires on forgot-password reset. Fixed to "when a user's password is reset via forgot password flow".

Tests

File Callback tested
sign-out.test.ts onLogout — verifies userId is passed correctly
password.test.ts onResetPasswordRequested — verifies call with user email
email-verification.test.ts onEmailVerificationRequested — verifies call on send
magic-link.test.ts onMagicLinkRequested — verifies call with email

Documentation

  • hooks.mdx: new "Lifecycle Callbacks" section with code examples for each callback, organized by category (Core, Email & Password, Email Verification, Plugins)

Changed files (18 files, +473 −50)

  • packages/core/src/types/init-options.ts — types for onLogin, onLogout, onPasswordChanged, onResetPasswordRequested, onEmailVerificationRequested
  • packages/better-auth/src/api/routes/sign-in.ts — onLogin implementation
  • packages/better-auth/src/api/routes/sign-out.ts — onLogout implementation
  • packages/better-auth/src/api/routes/update-user.ts — onPasswordChanged implementation
  • packages/better-auth/src/api/routes/password.ts — onResetPasswordRequested implementation
  • packages/better-auth/src/api/routes/email-verification.ts — onEmailVerificationRequested implementation
  • packages/better-auth/src/plugins/two-factor/types.ts + index.ts — onTotpEnabled / onTotpDisabled
  • packages/passkey/src/types.ts + routes.ts — onPasskeyAdded / onPasskeyDeleted
  • packages/better-auth/src/plugins/magic-link/index.ts — onMagicLinkRequested
  • Tests: sign-out.test.ts, password.test.ts, email-verification.test.ts, magic-link.test.ts
  • Docs: hooks.mdx

Summary by cubic

Adds lifecycle callbacks for security-sensitive auth events so apps can log, alert, or track analytics without global hooks. Callbacks run after each action and can be backgrounded.

  • New Features

    • Core: onLogin, onLogout.
    • Email & Password: onPasswordChanged, onResetPasswordRequested.
    • Email Verification: onEmailVerificationRequested.
    • Plugins: Two‑Factor onTotpEnabled/onTotpDisabled; Passkey onPasskeyAdded/onPasskeyDeleted in @better-auth/passkey; Magic Link onMagicLinkRequested in better-auth/plugins.
    • All callbacks run via runInBackgroundOrAwait; core callbacks use (data, request?) => Promise<void>.
    • Passkey: registration emits onPasskeyAdded; deletePasskey now accepts plugin options and emits onPasskeyDeleted.
  • Bug Fixes

    • Corrected onPasswordReset JSDoc to the forgot‑password flow.
    • Magic Link: wrapped onMagicLinkRequested with runInBackgroundOrAwait to avoid blocking responses.
    • Sign out: separated session lookup and deletion error handling; onLogout runs only after a successful deletion and includes userId.

Written for commit 8e4dd3865c46d83d9b1e3bb610cf2414d8918da9. Summary will update on new commits.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/better-auth/better-auth/pull/8915 **Author:** [@nphlp](https://github.com/nphlp) **Created:** 4/2/2026 **Status:** 🔄 Open **Base:** `next` ← **Head:** `feat/lifecycle-hooks` --- ### 📝 Commits (4) - [`d0f00a4`](https://github.com/better-auth/better-auth/commit/d0f00a4dded1cf7319a519055de00232ca0c3cf6) feat: add lifecycle event callbacks for security-sensitive operations - [`6959690`](https://github.com/better-auth/better-auth/commit/6959690c14df32697c931bbcf35532fec08490b9) fix: wrap onMagicLinkRequested with runInBackgroundOrAwait - [`5396115`](https://github.com/better-auth/better-auth/commit/5396115057fbb1e6945f891ad11816f510f10002) fix: separate findSession/deleteSession error handling in sign-out - [`8e4dd38`](https://github.com/better-auth/better-auth/commit/8e4dd3865c46d83d9b1e3bb610cf2414d8918da9) test: add onPasskeyAdded and onPasskeyDeleted callback tests ### 📊 Changes **19 files changed** (+552 additions, -44 deletions) <details> <summary>View changed files</summary> 📝 `.gitignore` (+5 -0) 📝 `docs/content/docs/concepts/hooks.mdx` (+168 -0) 📝 `packages/better-auth/src/api/routes/email-verification.test.ts` (+27 -0) 📝 `packages/better-auth/src/api/routes/email-verification.ts` (+8 -0) 📝 `packages/better-auth/src/api/routes/password.test.ts` (+15 -0) 📝 `packages/better-auth/src/api/routes/password.ts` (+8 -0) 📝 `packages/better-auth/src/api/routes/sign-in.ts` (+12 -0) 📝 `packages/better-auth/src/api/routes/sign-out.test.ts` (+13 -0) 📝 `packages/better-auth/src/api/routes/sign-out.ts` (+19 -0) 📝 `packages/better-auth/src/api/routes/update-user.ts` (+9 -0) 📝 `packages/better-auth/src/plugins/magic-link/index.ts` (+14 -0) 📝 `packages/better-auth/src/plugins/magic-link/magic-link.test.ts` (+24 -0) 📝 `packages/better-auth/src/plugins/two-factor/index.ts` (+19 -0) 📝 `packages/better-auth/src/plugins/two-factor/types.ts` (+16 -0) 📝 `packages/core/src/types/init-options.ts` (+40 -1) 📝 `packages/passkey/src/index.ts` (+1 -1) 📝 `packages/passkey/src/passkey.test.ts` (+75 -0) 📝 `packages/passkey/src/routes.ts` (+63 -42) 📝 `packages/passkey/src/types.ts` (+16 -0) </details> ### 📄 Description ## Summary Better Auth has generic `before`/`after` hooks (middleware that intercepts all endpoints), but there are no purpose-built callbacks for reacting to specific security events (login, logout, password change, etc.). This PR adds **lifecycle callbacks** — optional callbacks configured directly in the relevant options section, triggered after security-sensitive operations. They enable logging, analytics, security notifications, or any side-effect without writing a plugin or an `after` hook with `ctx.path` matching. ### Added callbacks **Core options:** - `onLogin` — triggered after a session is created (email/password, magic link, OAuth, passkey) - `onLogout` — triggered after a session is deleted **emailAndPassword options:** - `onPasswordChanged` — triggered when a user changes their password from their profile - `onResetPasswordRequested` — triggered when a password reset is requested (runs alongside `sendResetPassword`) **emailVerification options:** - `onEmailVerificationRequested` — triggered when a verification email is sent (runs alongside `sendVerificationEmail`) **Two-Factor plugin:** - `onTotpEnabled` / `onTotpDisabled` — triggered when 2FA is enabled/disabled **Passkey plugin:** - `onPasskeyAdded` / `onPasskeyDeleted` — triggered when a passkey is added/deleted **Magic Link plugin:** - `onMagicLinkRequested` — triggered when a magic link is sent (runs alongside `sendMagicLink`) ### Use cases - **Security notifications**: send an alert email when a user logs in from a new device (`onLogin`), changes their password (`onPasswordChanged`), or enables/disables 2FA (`onTotpEnabled`/`onTotpDisabled`) - **Audit / logging**: log security-sensitive events (logins, password resets, passkey additions/deletions) to an external system - **Analytics**: track authentication events (email verification rate, 2FA adoption, magic link usage) - **Abuse detection**: detect suspicious patterns (multiple password reset requests via `onResetPasswordRequested`) ### Technical decisions - **`runInBackgroundOrAwait`**: all callbacks use this existing pattern — they execute synchronously by default, or are deferred when a `backgroundTasks` handler is configured. - **Observational callbacks**: the `onXxxRequested` callbacks run *alongside* functional callbacks (`sendResetPassword`, `sendVerificationEmail`, `sendMagicLink`), they don't replace them. This separates email delivery (functional) from logging/analytics (observational). - **Consistent signature**: `(data: { ... }, request?: Request) => Promise<void>` for all core callbacks. Plugins follow their own context signature (`GenericEndpointContext` for magic-link). - **JSDoc** on every callback with clear description. ### Fix - `onPasswordReset`: JSDoc said "when a user's password is changed successfully" but it fires on *forgot-password reset*. Fixed to "when a user's password is reset via forgot password flow". ### Tests | File | Callback tested | |---|---| | `sign-out.test.ts` | `onLogout` — verifies userId is passed correctly | | `password.test.ts` | `onResetPasswordRequested` — verifies call with user email | | `email-verification.test.ts` | `onEmailVerificationRequested` — verifies call on send | | `magic-link.test.ts` | `onMagicLinkRequested` — verifies call with email | ### Documentation - **`hooks.mdx`**: new "Lifecycle Callbacks" section with code examples for each callback, organized by category (Core, Email & Password, Email Verification, Plugins) ### Changed files (18 files, +473 −50) - `packages/core/src/types/init-options.ts` — types for onLogin, onLogout, onPasswordChanged, onResetPasswordRequested, onEmailVerificationRequested - `packages/better-auth/src/api/routes/sign-in.ts` — onLogin implementation - `packages/better-auth/src/api/routes/sign-out.ts` — onLogout implementation - `packages/better-auth/src/api/routes/update-user.ts` — onPasswordChanged implementation - `packages/better-auth/src/api/routes/password.ts` — onResetPasswordRequested implementation - `packages/better-auth/src/api/routes/email-verification.ts` — onEmailVerificationRequested implementation - `packages/better-auth/src/plugins/two-factor/types.ts` + `index.ts` — onTotpEnabled / onTotpDisabled - `packages/passkey/src/types.ts` + `routes.ts` — onPasskeyAdded / onPasskeyDeleted - `packages/better-auth/src/plugins/magic-link/index.ts` — onMagicLinkRequested - Tests: `sign-out.test.ts`, `password.test.ts`, `email-verification.test.ts`, `magic-link.test.ts` - Docs: `hooks.mdx` <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Adds lifecycle callbacks for security-sensitive auth events so apps can log, alert, or track analytics without global hooks. Callbacks run after each action and can be backgrounded. - **New Features** - Core: `onLogin`, `onLogout`. - Email & Password: `onPasswordChanged`, `onResetPasswordRequested`. - Email Verification: `onEmailVerificationRequested`. - Plugins: Two‑Factor `onTotpEnabled`/`onTotpDisabled`; Passkey `onPasskeyAdded`/`onPasskeyDeleted` in `@better-auth/passkey`; Magic Link `onMagicLinkRequested` in `better-auth/plugins`. - All callbacks run via `runInBackgroundOrAwait`; core callbacks use `(data, request?) => Promise<void>`. - Passkey: registration emits `onPasskeyAdded`; `deletePasskey` now accepts plugin options and emits `onPasskeyDeleted`. - **Bug Fixes** - Corrected `onPasswordReset` JSDoc to the forgot‑password flow. - Magic Link: wrapped `onMagicLinkRequested` with `runInBackgroundOrAwait` to avoid blocking responses. - Sign out: separated session lookup and deletion error handling; `onLogout` runs only after a successful deletion and includes `userId`. <sup>Written for commit 8e4dd3865c46d83d9b1e3bb610cf2414d8918da9. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
GiteaMirror added the pull-request label 2026-04-13 10:34:00 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#16541