Password management

This guide covers the withManagedPassword mixin from @adonisplus/persona/password. You will learn how to:

  • Apply the mixin to your User model
  • Create password reset tokens
  • Reset passwords using tokens
  • Throttle token creation to prevent abuse

Overview

Password reset flows follow a common pattern: the user requests a reset, receives a token via email, and submits the token along with a new password. The withManagedPassword mixin handles the token lifecycle and password update atomically inside a database transaction.

The mixin adds a password column to your model and provides methods for creating reset tokens, verifying them, and updating the password in a single operation.

Setup

The withManagedPassword mixin needs a database table for storing reset tokens and must be applied to your User model.

  1. Create the password reset tokens table

    Generate a migration for the password_reset_tokens table. This table stores hashed reset tokens, each tied to a user.

    node ace make:migration create_password_reset_tokens
    database/migrations/xxxx_create_password_reset_tokens.ts
    import { BaseSchema } from '@adonisjs/lucid/schema'
    
    export default class extends BaseSchema {
      protected tableName = 'password_reset_tokens'
    
      async up() {
        this.schema.createTable(this.tableName, (table) => {
          table.increments('id')
          table.integer('tokenable_id').notNullable().unsigned()
          table.string('hash', 80).notNullable()
          table.timestamp('created_at', { precision: 6, useTz: true }).notNullable()
          table.timestamp('expires_at', { precision: 6, useTz: true }).nullable()
        })
      }
    
      async down() {
        this.schema.dropTable(this.tableName)
      }
    }
  2. Apply the mixin

    Apply the withManagedPassword mixin to your User model using compose.

    app/models/user.ts
    import { UserSchema } from '#database/schema'
    import { compose } from '@adonisjs/core/helpers'
    import { withManagedPassword } from '@adonisplus/persona/password'
    
    export default class User extends compose(
      UserSchema,
      withManagedPassword() 
    ) {}

    You can customize the token provider by passing a config object.

    app/models/user.ts
    export default class User extends compose(
      UserSchema,
      withManagedPassword({
        table: 'custom_reset_tokens',
        expiresIn: '2 hours',
      })
    ) {}
    table
    string

    The database table used to store password reset tokens.

    Defaults to "password_reset_tokens".

    tokenSecretLength
    number

    The length of the cryptographically secure random string used as the token secret.

    Defaults to 40.

    expiresIn
    string | number

    How long tokens remain valid. Accepts a number in seconds or a time expression string like '1 day' or '2 hours'.

    Defaults to '1 day'.

Creating reset tokens

When a user requests a password reset, look them up by email and create a reset token. The token value is an opaque string safe to include in a URL.

const user = await User.findByOrFail('email', email)
const token = await user.createPasswordResetToken()

// Send rawTokenValue in the email
const rawTokenValue = token.value!.release()

You should enable throttling on user-facing endpoints to prevent a single user from flooding the tokens table. The first argument enables throttling. The second sets the throttle window (defaults to 60 seconds). When a token was created within the window, the method returns null.

const token = await user.createPasswordResetToken(true, '1 min')

Resetting the password

When the user submits the token along with a new password, pass both to the static resetPassword method. This method verifies the token, finds the user, and updates the password inside a database transaction.

const user = await User.resetPassword(token, newPassword)

/**
 * You must call `clearPasswordResetTokens()` after a
 * successful reset.
 */
await user.clearPasswordResetTokens()

If the reset fails (invalid token, expired token, or user not found), the method throws an E_INVALID_PASSWORD_TOKEN exception. The exception includes a built-in handle method that content-negotiates the response:

  • HTML requests: Flashes the error message to the session and redirects. You can customize the redirect URL by passing options.
  • JSON requests: Returns { errors: [{ message: "..." }] } with a 400 status.
  • JSONAPI requests: Returns { errors: [{ code: "E_INVALID_PASSWORD_TOKEN", title: "..." }] }.
  • i18n: If you have @adonisjs/i18n configured, the error message is translated using the errors.E_INVALID_PASSWORD_TOKEN key.
const user = await User.resetPassword(token, newPassword, {
  redirectTo: '/login',
})

Instance properties and methods

The withManagedPassword mixin adds the following to model instances.

password
string

The hashed password. Mapped to the password database column.

createPasswordResetToken
method

Creates a password reset token for the user.

// Without throttling
const token = await user.createPasswordResetToken()

// With throttling (returns null within the window)
const token = await user.createPasswordResetToken(true, '1 min')
clearPasswordResetTokens
method

Deletes all password reset tokens for the user. Returns the number of deleted rows.

await user.clearPasswordResetTokens()

Static properties and methods

passwordResetTokens
DbPasswordTokensProvider

The token provider instance attached to the model. You can use it directly for lower-level token operations.

const tokens = await User.passwordResetTokens.all(user)
const lastCreated = await User.passwordResetTokens.lastCreatedAt(user)
await User.passwordResetTokens.delete(user, tokenId)
resetPassword
static method

Resets the user's password using a token value string. Runs inside a database transaction. Returns the user instance with the updated password.

Throws E_INVALID_PASSWORD_TOKEN when the token is invalid, expired, or the user is not found.

const user = await User.resetPassword(tokenValue, newPassword)
const user = await User.resetPassword(tokenValue, newPassword, { redirectTo: '/login' })
Terms & License Agreement