DocsHub
Security

Authentication

Learn the two layers of authentication in a MongoDB-backed app — database-level MongoDB authentication and application-level user authentication with hashed passwords and JWT.

Authentication

Authentication answers one question — who are you? In a MongoDB-backed application, there are actually two separate layers of authentication, and beginners often confuse them.

  • MongoDB database authentication — controls who can connect to your MongoDB server at all
  • Application authentication — controls who can log into your app (students, teachers, admins)

These are completely independent systems. Your app's users (students, teachers) never directly authenticate with MongoDB — only your application server does.


The Two Layers

Your Application MongoDB Server Layer 1:MongoDB AuthenticationDatabase username + password Layer 2:Application AuthenticationJWT / Session School App User(Teacher logs in withemail + password) App Server(Node.js) school database

Layer 1 — MongoDB Authentication: Your application server connects to MongoDB using database credentials (a username and password configured on the MongoDB server itself). This happens once, when your server starts.

Layer 2 — Application Authentication: Individual users of your app (teachers, students, admins) log in through your app's login system. Your app verifies their credentials and issues a session or token. MongoDB never sees these individual users — it only knows about your one application database user.


Layer 1 — MongoDB Database Authentication

By default, a fresh MongoDB installation has no authentication enabled — anyone who can reach the server can read and write all data. This is fine for local development but must never happen in production.

Enabling Authentication

MongoDB authentication is enabled in the configuration file (mongod.conf):

security:
  authorization: enabled

Once enabled, every connection to MongoDB must provide valid credentials.

Creating an Admin User

Before enabling authentication, create at least one admin user. Connect to MongoDB and run:

use admin

db.createUser({
  user: "adminUser",
  pwd: "strongPasswordHere",
  roles: [{ role: "userAdminAnyDatabase", db: "admin" }]
})

Creating an Application User

For our school app, create a dedicated user with access to only the school database — never use the admin user in your application code.

use school

db.createUser({
  user: "schoolAppUser",
  pwd: "anotherStrongPassword",
  roles: [{ role: "readWrite", db: "school" }]
})

This user can read and write to the school database only — it cannot access other databases, create users, or perform admin operations.

Connecting with the Application User

// .env
MONGODB_URI=mongodb://schoolAppUser:anotherStrongPassword@localhost:27017/school
await mongoose.connect(process.env.MONGODB_URI);

Never use a MongoDB user with admin privileges in your application's connection string. If your application server is compromised, an attacker with admin credentials could access every database on the server — not just yours. Always create a dedicated, limited-privilege user for each application.


Layer 2 — Application Authentication

This is the login system for your school app — teachers and admins log in with an email and password, and your app verifies their identity.

Storing Passwords Safely

Never store plain text passwords. We covered this briefly in the Mongoose Middleware file — here is the complete picture for our school system.

const mongoose = require('mongoose');
const bcrypt = require('bcrypt');

const userSchema = new mongoose.Schema(
  {
    name: { type: String, required: true },
    email: {
      type: String,
      required: true,
      unique: true,
      lowercase: true,
      match: [/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, "Invalid email"]
    },
    password: {
      type: String,
      required: true,
      minlength: [8, "Password must be at least 8 characters"]
    },
    role: {
      type: String,
      enum: ["admin", "teacher", "student"],
      required: true
    }
  },
  { timestamps: true }
);

// Hash password before saving
userSchema.pre('save', async function (next) {
  if (!this.isModified('password')) return next();
  this.password = await bcrypt.hash(this.password, 10);
  next();
});

// Instance method to compare passwords
userSchema.methods.comparePassword = async function (candidatePassword) {
  return bcrypt.compare(candidatePassword, this.password);
};

module.exports = mongoose.model('User', userSchema);

bcrypt — How It Works

bcrypt.hash(password, 10) takes a plain text password and produces a hash — a one-way scrambled version. The 10 is the salt rounds — how computationally expensive the hashing is. Higher numbers are more secure but slower.

const plainPassword = "mypassword123";
const hashed = await bcrypt.hash(plainPassword, 10);

console.log(hashed);
// "$2b$10$N9qo8uLOickgx2ZMRZoMye.IjZAgcfl7p92ldGxad68LJZdL17lhW"

You cannot reverse a hash back to the original password. To check a login attempt, you hash the attempt and compare:

const isMatch = await bcrypt.compare(plainPassword, hashed);
console.log(isMatch); // true

bcrypt.compare() does not "decrypt" the stored hash — it hashes the input with the same salt and checks if the results match.


Login Flow

const User = require('./models/User');

async function login(email, password) {
  // Find user by email
  const user = await User.findOne({ email: email.toLowerCase() });

  if (!user) {
    throw new Error('Invalid email or password');
  }

  // Compare password
  const isMatch = await user.comparePassword(password);

  if (!isMatch) {
    throw new Error('Invalid email or password');
  }

  return user;
}

Always return the same error message — "Invalid email or password" — whether the email does not exist or the password is wrong. If you say "Email not found" vs "Wrong password" separately, you reveal to an attacker whether a given email is registered in your system. This is called a user enumeration vulnerability.


JWT — JSON Web Tokens

After a successful login, your app needs a way to remember that this user is logged in for subsequent requests — without requiring them to send their email and password on every request.

JWT (JSON Web Token) is a compact, signed token that your server issues after login. The client stores it and sends it with future requests. Your server verifies the signature to confirm the token is genuine and has not been tampered with.

Installing jsonwebtoken

npm install jsonwebtoken

Issuing a Token on Login

const jwt = require('jsonwebtoken');

async function login(email, password) {
  const user = await User.findOne({ email: email.toLowerCase() });

  if (!user || !(await user.comparePassword(password))) {
    throw new Error('Invalid email or password');
  }

  // Create a token containing the user's id and role
  const token = jwt.sign(
    { userId: user._id, role: user.role },
    process.env.JWT_SECRET,
    { expiresIn: '7d' }
  );

  return { token, user: { id: user._id, name: user.name, role: user.role } };
}

process.env.JWT_SECRET is a long, random string stored in your environment variables — never hardcoded, never committed to version control. Anyone with this secret can forge tokens.

Verifying a Token

When a request comes in with a token, verify it before processing:

function verifyToken(token) {
  try {
    return jwt.verify(token, process.env.JWT_SECRET);
    // returns { userId: "...", role: "...", iat: ..., exp: ... }
  } catch (error) {
    throw new Error('Invalid or expired token');
  }
}

Authentication Middleware in Express

function requireAuth(req, res, next) {
  const authHeader = req.headers.authorization; // "Bearer <token>"

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ message: 'No token provided' });
  }

  const token = authHeader.split(' ')[1];

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded; // { userId, role }
    next();
  } catch (error) {
    return res.status(401).json({ message: 'Invalid or expired token' });
  }
}

Using the Middleware

const express = require('express');
const app = express();

// Public route — no auth required
app.post('/api/login', async (req, res) => {
  try {
    const { email, password } = req.body;
    const result = await login(email, password);
    res.json(result);
  } catch (error) {
    res.status(401).json({ message: error.message });
  }
});

// Protected route — requires valid token
app.get('/api/profile', requireAuth, async (req, res) => {
  const user = await User.findById(req.user.userId).select('-password');
  res.json(user);
});
GET /api/profile
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Complete Login Flow — School System

const express = require('express');
const jwt = require('jsonwebtoken');
const User = require('./models/User');

const app = express();
app.use(express.json());

// Register — only admins can create teacher/admin accounts
app.post('/api/register', async (req, res) => {
  try {
    const { name, email, password, role } = req.body;

    const user = await User.create({ name, email, password, role });

    res.status(201).json({
      message: 'User created',
      user: { id: user._id, name: user.name, email: user.email, role: user.role }
    });
  } catch (error) {
    if (error.name === 'ValidationError') {
      return res.status(400).json({ message: error.message });
    }
    if (error.code === 11000) {
      return res.status(409).json({ message: 'Email already registered' });
    }
    res.status(500).json({ message: 'Server error' });
  }
});

// Login
app.post('/api/login', async (req, res) => {
  try {
    const { email, password } = req.body;

    const user = await User.findOne({ email: email.toLowerCase() });

    if (!user || !(await user.comparePassword(password))) {
      return res.status(401).json({ message: 'Invalid email or password' });
    }

    const token = jwt.sign(
      { userId: user._id, role: user.role },
      process.env.JWT_SECRET,
      { expiresIn: '7d' }
    );

    res.json({
      token,
      user: { id: user._id, name: user.name, role: user.role }
    });
  } catch (error) {
    res.status(500).json({ message: 'Server error' });
  }
});

module.exports = app;

Quick Reference

ConceptWhat it is
MongoDB authenticationServer-level — controls who can connect to the database
Application authenticationApp-level — controls who can log into your app
bcrypt.hash()One-way hash for storing passwords
bcrypt.compare()Verify a password attempt against the stored hash
JWTSigned token proving a user is logged in
jwt.sign()Create a token after successful login
jwt.verify()Check a token is valid and not tampered with

Keep these two layers mentally separate. MongoDB authentication is a one-time setup concern — configure it once when you set up your database. Application authentication is an ongoing concern that touches every part of your app — login, registration, protected routes, and role checks. The Authorization file builds on this foundation to control what each logged-in user is allowed to do.

On this page