Overview
User registration involves taking user input (username + password), safely storing the account in the database, and ensuring passwords are not recoverable even if the database is leaked. The main ideas:
- Accept registration POST from the browser
- Validate input on server side
- Hash the password (with a salt) before saving
- Store only the username and hashed password (never the plain password)
- Consider email verification, HTTPS, rate limiting, etc.
Step-by-step Registration Flow
- User fills form on the client (browser) with
usernameandpasswordand clicks Register. - The browser sends an HTTP POST to your server endpoint, e.g.
POST /register. - Server receives the request (Express route). Parse the body (JSON or form-data).
- Server validates username/password (length, allowed chars, password strength).
- Server hashes the password using a secure algorithm (e.g.
bcrypt) —bcryptautomatically generates a salt and stores it inside the final hash. - Save the user record (username + passwordHash + any metadata) into MongoDB via Mongoose.
- Return success or appropriate error (e.g., user already exists). Optionally send verification email.
Why Hash + Salt?
- Hashing converts a password into a fixed-length string (the hash). Hash functions are one-way: you can’t recover the original password from the hash.
- Salting prevents precomputed attacks (rainbow tables). A salt is random data mixed with the password before hashing. Modern libraries like
bcryptautomatically generate and embed the salt in the resulting hash string — you don’t need to handle salt storage separately. - If hashes are leaked, attackers must individually brute-force or guess each password. Proper hashing (bcrypt/Argon2) and salts make this extremely slow.
Recommended Tools
- Node.js + Express for server
- MongoDB for the database
- Mongoose for ODM (schema + model)
- bcrypt (or
bcryptjs) for password hashing — or consider Argon2 for even stronger protection
Example: Minimal Registration Code
Install packages
npm install express mongoose bcrypt body-parser
User model (Mongoose) — models/User.js
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true, lowercase: true, trim: true },
passwordHash: { type: String, required: true },
createdAt: { type: Date, default: Date.now },
// optional fields: emailVerified, roles, profile, etc.
});
module.exports = mongoose.model('User', userSchema);
Express route with bcrypt — routes/auth.js
const express = require('express');
const router = express.Router();
const bcrypt = require('bcrypt');
const User = require('../models/User');
// config
const BCRYPT_SALT_ROUNDS = 10; // 10-12 is common, higher is slower but more secure
router.post('/register', async (req, res) => {
try {
const { username, password } = req.body;
// Basic validation (do more in production)
if (!username || !password || password.length < 8) {
return res.status(400).json({ message: 'Invalid input' });
}
// Check if user exists
const existing = await User.findOne({ username: username.toLowerCase() });
if (existing) return res.status(409).json({ message: 'User already exists' });
// Hash password with bcrypt (bcrypt generates salt internally)
const passwordHash = await bcrypt.hash(password, BCRYPT_SALT_ROUNDS);
// Create user
const user = new User({ username: username.toLowerCase(), passwordHash });
await user.save();
// Optional: send verification email here
return res.status(201).json({ message: 'User registered' });
} catch (err) {
console.error(err);
return res.status(500).json({ message: 'Server error' });
}
});
module.exports = router;
Login (check password) example:
router.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = await User.findOne({ username: username.toLowerCase() });
if (!user) return res.status(401).json({ message: 'Invalid credentials' });
// bcrypt.compare will use the salt embedded into the stored hash
const ok = await bcrypt.compare(password, user.passwordHash);
if (!ok) return res.status(401).json({ message: 'Invalid credentials' });
// issue session or JWT
return res.json({ message: 'Login OK' });
});
Important Security Best Practices
- Use HTTPS for all authentication/registration requests. Never send passwords over plain HTTP.
- Use a modern password hash:
bcrypt,scrypt, or Argon2 (Argon2 is recommended if available). - Do not store plaintext passwords or reversible encryption.
- Use a reasonable bcrypt cost (salt rounds). Higher rounds = slower hashing = more resistant to brute force. Typical: 10–14, tune for your server CPU.
- Rate-limit authentication endpoints to mitigate credential stuffing and brute-force attempts.
- Require strong passwords (min length + complexity) and consider password strength meters on frontend.
- Email verification: confirm email addresses before granting full access.
- Account lockout / progressive delay after repeated failed attempts.
- Use prepared/parameterized queries (Mongoose already helps), avoid injection attacks.
- Store minimal user info (avoid sensitive metadata unless required).
- Regularly rotate secrets (e.g., JWT signing keys) and keep dependencies updated.
- Log and monitor suspicious activity; never log raw passwords.
Common Misconceptions
- “I’ll store salt separately.” Modern libraries like
bcryptembed salt in the hash string; you don’t need a separate DB column. - “Hashing alone is enough.” Hashing + salt + high cost factor + HTTPS + rate-limiting are all needed together.
- “MD5/SHA1 are fine.” They are not secure for password hashing. Use bcrypt/Argon2/scrypt.
Extra Features to Add Later
- Email verification workflow (send token, confirm).
- Password reset via email token (expires quickly).
- Two-Factor Authentication (2FA) (TOTP or SMS).
- Session management / JWT with refresh tokens.
- OAuth social login (Google, Facebook, etc.).
- Audit logs for critical account actions.
Summary
- Registration flow: browser →
POST /register→ validate → hash (with salt) → save hashed password in MongoDB. - Use
bcrypt(or Argon2) to hash; do not store plain passwords. - Combine secure hashing with HTTPS, rate-limiting, validation, and account verification for a robust authentication system.
