Credentials authentication

Email and password login via NextAuth v5 Credentials provider with bcrypt password comparison.

How it works

The Credentials provider exposes an authorize() function that NextAuth calls when signIn('credentials', ...) is invoked. It receives the raw form credentials, validates them, and returns a user object on success or null on failure.

The authorize function

lib/auth.ts
CredentialsProvider({
  async authorize(credentials) {
    try {
      const parsed = loginSchema.safeParse(credentials)
      if (!parsed.success) return null

      await connectMongo()
      const user = await UserModel
        .findOne({ email: parsed.data.email })
        .select('+password')

      if (!user) return null

      const valid = await user.comparePassword(parsed.data.password)
      if (!valid) return null

      return {
        id: user._id.toString(),
        name: user.name,
        email: user.email,
        role: user.role,
        emailVerified: user.emailVerified,
      }
    } catch {
      return null
    }
  }
})

Password hashing

Passwords are hashed using bcryptjs at cost factor 12 via a Mongoose pre-save hook:

lib/db/models/user.model.ts
userSchema.pre('save', async function (next) {
  if (!this.isModified('password')) return next()
  try {
    this.password = await bcrypt.hash(this.password, 12)
    next()
  } catch (err) {
    next(err as Error)
  }
})

Cost factor 12 means approximately 300ms per hash on modern hardware - enough to make brute force attacks computationally expensive without degrading user experience.

JWT and session callbacks

Two callbacks ensure user data is available in the JWT and propagated to the session:

lib/auth.ts
callbacks: {
  async jwt({ token, user }) {
    if (user) {
      token.id = user.id
      token.role = user.role
      token.emailVerified = user.emailVerified
    }
    return token
  },
  async session({ session, token }) {
    session.user.id = token.id as string
    session.user.role = token.role as string
    session.user.emailVerified = token.emailVerified as boolean
    return session
  }
}

Customising

Adding fields to the session

To add a new field to the session (e.g. subscription tier):

  1. Add the field to the User model
  2. Return it from authorize()
  3. Add it to the jwt callback (token.subscriptionTier = user.subscriptionTier)
  4. Add it to the session callback (session.user.subscriptionTier = token.subscriptionTier)
  5. Add it to types/next-auth.d.ts augmentation