User Authentication Systems
Almost every app on MongoDB stores users, sessions and identity in MongoDB itself.
Introduction
Almost every app on MongoDB stores users, sessions and identity in MongoDB itself. The document model is ideal: roles, permissions, MFA secrets, OAuth identities and audit history all sit naturally on the same user document, with embedded sub-documents and arrays.
This lesson covers the production-grade user authentication system: password hashing, JWT/session storage, refresh tokens, OAuth identity linking, MFA, lockouts and audit. Frameworks like NextAuth, Auth.js, Passport and Spring Security all integrate seamlessly.
Understanding the topic
The user document — battle-tested shape:
email+ unique index, normalized lowercase.passwordHashwith argon2id or bcrypt(cost ≥ 12).identities[]— array of OAuth providers ({ provider, sub, linkedAt }).mfa: { totpSecret, recoveryCodes, enabledAt }.sessionsin a separate collection with TTL index onexpiresAt.loginAttempts+lockedUntilfor brute-force protection.auditLog[]embedded array (capped at 50) for last logins, device fingerprints, IP.
Informative example
Sessions collection with TTL — sessions self-clean:
// One-time setupdb.sessions.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 });db.sessions.createIndex({ userId: 1, createdAt: -1 });// Createawait db.sessions.insertOne({_id: crypto.randomUUID(),userId,refreshTokenHash: sha256(refreshToken),device: { ua, ip, country },createdAt: new Date(),expiresAt: new Date(Date.now() + 30 * 24 * 3600 * 1000)});// Rotate refresh tokenawait db.sessions.findOneAndUpdate({ _id: sessionId, refreshTokenHash: sha256(oldRt) },{ $set: { refreshTokenHash: sha256(newRt), expiresAt: new Date(Date.now() + 30*864e5) } });
Real-world use
NextAuth.js, Auth.js, Clerk and Supabase Auth (in MongoDB-backed deployments) all use this shape. Argon2id + per-session refresh tokens + TTL-based expiry is the modern baseline.
Best practices
- Hash with argon2id; bcrypt cost 12+ is acceptable on legacy systems.
- Store refresh tokens hashed — never plaintext, even in the DB.
- Use a TTL index on
sessions.expiresAtfor automatic cleanup. - Rate-limit login attempts in the app layer; lock the account after 10 fails.
Common mistakes
- Storing the JWT secret in the same DB it protects.
- Skipping refresh-token rotation — a stolen refresh token = lifetime account access.
- Embedding sessions inside the user document — the doc grows unbounded.