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.
Never throw errors from authorize() - returning null signals a failed login. NextAuth handles the error routing to the signIn page.
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 uses select: false in the Mongoose schema - it is never returned in normal queries. The .select('+password') opt-in is required here to retrieve it for comparison.
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):
- Add the field to the User model
- Return it from authorize()
- Add it to the jwt callback (token.subscriptionTier = user.subscriptionTier)
- Add it to the session callback (session.user.subscriptionTier = token.subscriptionTier)
- Add it to types/next-auth.d.ts augmentation