Nest Authbeta

Hooks Reference

Every config-time hook with its execution-order timeline.

This is the exhaustive hook reference. For the conceptual overview of when to use a hook vs an event, see Events & Hooks.

Execution-order timeline — signup flow

POST /auth/signup
  │
  ├─► registrationHooks.beforeSignup(payload, ctx)
  │     • can mutate the payload
  │     • can throw to abort (e.g. weak password, banned email)
  │
  ├─► user.beforeCreate(input)
  │     • last chance to mutate the user entity before INSERT
  │
  ├─► [INSERT INTO nest_auth_users]
  │
  ├─► user.afterCreate(user)
  │     • sync side effects that must succeed
  │
  ├─► registrationHooks.onSignup(user, payload)
  │     • assign roles here — they land in the FIRST JWT
  │     • this is your last chance before the session is built
  │
  ├─► user.getSessionUserData(user, helpers)  ← also called on subsequent logins
  │     • shape what goes into the session payload
  │
  ├─► session.customizeSessionData(default, user)
  ├─► session.customizeTokenPayload(default, session)
  │     • final shaping of session row + JWT claims
  │
  ├─► [CREATE SESSION + SIGN JWT]
  │
  ├─► session.onCreated(session, user)         ← async, fire-and-forget
  │
  └─► UserRegisteredEvent emitted             ← your event listeners run

Execution-order timeline — login flow

POST /auth/login
  │
  ├─► guards.beforeAuth(request)               ← runs on EVERY guarded request
  │     • { reject: true, reason: '...' } to short-circuit
  │
  ├─► [resolve credentials → user]
  │
  ├─► loginHooks.onLogin(user, ctx)
  │     • ctx includes userAccess + platformAccess
  │     • use to sync external roles, write last-login, etc.
  │
  ├─► authorization.resolveRoles(user)         ← if you replaced the default
  ├─► authorization.resolvePermissions(user, roles)
  │
  ├─► user.getSessionUserData → customizeSessionData → customizeTokenPayload
  │     (same as signup)
  │
  ├─► [CREATE SESSION + SIGN JWT]
  │
  ├─► session.onCreated(session, user)
  ├─► UserLoggedInEvent emitted
  │
  └─► guards.afterAuth(request, user)          ← last chance to reject

Execution-order timeline — refresh flow

POST /auth/refresh-token
  │
  ├─► [validate refresh token → session row]
  │
  ├─► authorization.resolveRoles / resolvePermissions  (re-evaluated)
  ├─► user.getSessionUserData                          (re-evaluated)
  ├─► customizeSessionData / customizeTokenPayload     (re-applied)
  │
  ├─► [SIGN NEW ACCESS TOKEN]
  │
  └─► session.onRefreshed(oldSession, newSession)

Hook reference

registrationHooks.beforeSignup(payload, ctx) => Promise<payload> | payload

Gate or mutate signup. Throw to abort.

registrationHooks: {
  async beforeSignup(payload, ctx) {
    if (await this.banlist.has(payload.email)) {
      throw new ForbiddenException('email_blacklisted');
    }
    return { ...payload, email: payload.email.toLowerCase() };
  },
},

registrationHooks.onSignup(user, payload) => Promise<void>

Runs after the user is inserted but before the first session is built. Assign initial roles here so they land in the first JWT:

registrationHooks: {
  async onSignup(user, payload) {
    if (payload.invitationToken) {
      const role = await this.invites.consume(payload.invitationToken);
      await this.userAccess.assignRole(user.id, role);
    }
  },
},

user.beforeCreate(input) => input | Promise<input>

Final mutation before the user row is inserted.

user.afterCreate(user, input, manager) => Promise<void>

Sync side effects after the user (plus its default access and identities) are inserted. Runs inside the creation transaction — throwing rolls the whole create back, so a partial user can never be persisted. Use the third argument manager (a TypeORM EntityManager) for any DB writes that must commit or roll back together with the user:

user: {
  async afterCreate(user, input, manager) {
    // This insert is part of the same transaction — if it throws, the auth
    // user is rolled back too. No orphaned rows on either side.
    await manager.getRepository(AppUser).insert({ authUserId: user.id });
  },
},

user.beforeUpdate(user, changes, manager) => Partial<NestAuthUser> | void

Runs inside the update transaction before the row is saved. Return a partial to merge into changes, mutate changes in place, or throw to abort the update.

user.afterUpdate(user, changes, manager) => Promise<void>

Runs inside the update transaction after the row is saved. Throwing rolls the update back. changes is the set of fields that changed.

user.beforeDelete(user, manager) => Promise<void>

Runs inside the delete transaction while the row still exists. Throw to abort the deletion. Use manager to remove related rows so they roll back together.

user.afterDelete(user, manager) => Promise<void>

Runs inside the delete transaction after removal (receives a snapshot of the deleted user). Throwing rolls the deletion back.

Atomicity. User create, update, and delete each run in a single transaction. If any hook (or a related DB write you make via manager) throws, the entire operation rolls back — there are no partially-created or half-updated users. Lifecycle events (user.created / updated / deleted) are emitted only after the transaction commits.

user.getSessionUserData(user, helpers) => SessionUserData | Promise<SessionUserData>

The most-called hook: runs on signup, login, and refresh. Use it to inject AppUser fields into the session payload.

user: {
  async getSessionUserData(authUser, helpers) {
    const appUser = await helpers.dataSource
      .getRepository(AppUser)
      .findOne({ where: { authUserId: authUser.id } });
    return {
      firstName: appUser?.firstName,
      avatarUrl: appUser?.avatarUrl,
    };
  },
},

user.sensitiveFields: string[]

List of NestAuthUser fields to never serialize into responses (e.g., 'metadata.internalNotes').

loginHooks.onLogin(user, ctx) => Promise<void>

ctx carries request, userAccess, platformAccess, and (when the login auto-creates a user) manager. Useful for syncing external systems on every login. registrationHooks.onSignup likewise receives { request, manager } so its writes can join the signup transaction.

These hooks are fully typed — input is the NestAuthSignupRequestDto / NestAuthLoginRequestDto, and ctx.request is the Express Request (no more any). When you can't rely on contextual typing, annotate explicitly with the exported context interfaces: IBeforeSignupContext, IOnSignupContext, IOnLoginContext.

session.customizeSessionData(default, user) => SessionDataPayload

Reshape the snapshot stored on the session row. Don't bloat — this is loaded on every refresh.

session.customizeTokenPayload(default, session) => JWTTokenPayload

Final claims for the JWT. Anything you add here is in the access token. Keep it small.

session.onCreated / onRefreshed / onRevoked

Fire-and-forget. For audit, cache invalidation, etc.

onRevoked(session, reason) receives one of 'logout', 'expired', 'admin', 'security', 'password_change' or 'evicted'. 'evicted' means a new sign-in pushed the account past maxSessionsPerUser and the least-recently-active session was dropped to make room — before 2.14 evictions bypassed this hook and were silent.

authorization.resolveRoles(user) => Promise<string[]>

Replaces the default DB lookup. Return whatever roles the user has.

authorization.resolvePermissions(user, roles) => Promise<string[]>

Replaces the default role→permission expansion. Note: this feeds nest-auth's own permission check only — it does not rewrite session.data.permissions (see liveSessionRoles below for keeping that copy current).

authorization.liveSessionRoles: boolean | { ttl }

Keeps a signed-in session's session.data.roles / .permissions in sync with the database. Sign-in copies them into the session, and before 2.12 nothing refreshed that copy — a permission added to a role, a role granted or revoked, or a membership going inactive never reached anyone already signed in. The guard now re-resolves them from the same rows sign-in reads, on a per-instance cache.

  • true (default) — refresh with a 30s cache window.
  • { ttl: '10s' } — tune the window (ms-style string or milliseconds; 0 re-resolves every request).
  • false — restore the pre-2.12 behavior (frozen at sign-in).

Only roles and permissions are replaced (in memory, per request); everything else in session.data — including fields from session.customizeSessionData — is untouched. Apps that inject extra roles/permissions at sign-in via that hook should re-apply them in guards.afterAuth (which runs after the refresh) or set this to false. A failed read keeps the sign-in copy; it never turns into a 401. The cache is per process, so on multi-instance deployments a change propagates within ttl on each instance.

guards.beforeAuth(request) => { reject?, reason? } | Promise<…>

guards: {
  beforeAuth(req) {
    const ip = req.ip;
    if (!this.allowlist.has(ip)) return { reject: true, reason: 'ip_blocked' };
  },
},

guards.afterAuth(request, user) => { reject?, reason? } | Promise<…>

Same shape, runs after auth succeeded. Useful for device fingerprint check that depends on user.

errorHandler(error, context) => any

context is one of 'login' | 'signup' | 'password_reset' | 'mfa_verify' | …. Transform the error into your app's standard shape.

resolveConfig(context) => Partial<IAuthModuleOptions> | Promise<…>

Per-request config override. The most common use is mode-switching:

resolveConfig(ctx) {
  const isMobile = ctx.request.headers['x-app'] === 'mobile';
  return {
    session: { accessTokenType: isMobile ? 'header' : 'cookie' },
  };
},

clientConfig.factory(default, context) => any

Reshapes the public /auth/client-config response — what your frontend reads to decide which sign-in buttons to render. The default already includes email/phone/passwordless flags, OAuth public client/app ids, MFA, tenants, platformAccess.enabled, and accessTokenType. Use the factory to add branding (ui), SSO, tenant option lists, or other app-specific fields.

password.hash(password) => Promise<string> / password.verify(password, hash) => Promise<boolean>

Drop-in replacements for argon2. Use this if your security team mandates a specific algorithm.

otp.generate(length, format) => string | Promise<string>

Drop-in replacement for the default OTP generator.

audit.onEvent(event) => Promise<void> | void

Sink for audit events. See Audit Logging.

On this page

Execution-order timeline — signup flowExecution-order timeline — login flowExecution-order timeline — refresh flowHook referenceregistrationHooks.beforeSignup(payload, ctx) => Promise<payload> | payloadregistrationHooks.onSignup(user, payload) => Promise<void>user.beforeCreate(input) => input | Promise<input>user.afterCreate(user, input, manager) => Promise<void>user.beforeUpdate(user, changes, manager) => Partial<NestAuthUser> | voiduser.afterUpdate(user, changes, manager) => Promise<void>user.beforeDelete(user, manager) => Promise<void>user.afterDelete(user, manager) => Promise<void>user.getSessionUserData(user, helpers) => SessionUserData | Promise<SessionUserData>user.sensitiveFields: string[]loginHooks.onLogin(user, ctx) => Promise<void>session.customizeSessionData(default, user) => SessionDataPayloadsession.customizeTokenPayload(default, session) => JWTTokenPayloadsession.onCreated / onRefreshed / onRevokedauthorization.resolveRoles(user) => Promise<string[]>authorization.resolvePermissions(user, roles) => Promise<string[]>authorization.liveSessionRoles: boolean | { ttl }guards.beforeAuth(request) => { reject?, reason? } | Promise<…>guards.afterAuth(request, user) => { reject?, reason? } | Promise<…>errorHandler(error, context) => anyresolveConfig(context) => Partial<IAuthModuleOptions> | Promise<…>clientConfig.factory(default, context) => anypassword.hash(password) => Promise<string> / password.verify(password, hash) => Promise<boolean>otp.generate(length, format) => string | Promise<string>audit.onEvent(event) => Promise<void> | voidRelated