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

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.

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.

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[]>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