Node & Express ⚡ Recently updatedUpdated on Jun 30, 2026 — this content may still be evolving.

Node.js & Express: Auth & Security

Practical auth and security for student-level backend interviews.


Password Storage

Never store plain-text passwords.

const bcrypt = require('bcrypt');
const SALT_ROUNDS = 12;

// Register
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
await User.create({ email, password: hashedPassword });

// Login
const user = await User.findOne({ email });
const match = await bcrypt.compare(password, user.password);
if (!match) return res.status(401).json({ error: 'Invalid credentials' });

Interview point: bcrypt is slow by design (resists brute force). Salt is embedded in the hash.


JWT Authentication

Structure

header.payload.signature — base64url encoded. Signature prevents tampering.

Sign on Login

const jwt = require('jsonwebtoken');

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

res.json({ token, user: { id: user._id, email: user.email } });

Verify Middleware

function authMiddleware(req, res, next) {
  const header = req.headers.authorization;
  if (!header?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No token provided' });
  }

  const token = header.split(' ')[1];
  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    req.user = payload;
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid or expired token' });
  }
}

// Protected route
app.get('/api/profile', authMiddleware, (req, res) => {
  res.json({ userId: req.user.userId });
});

Refresh Tokens (awareness)

Access token: short-lived (15 min). Refresh token: long-lived, stored httpOnly, used to get new access token. Reduces exposure if access token leaks.


Session-Based Auth

const session = require('express-session');

app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    maxAge: 24 * 60 * 60 * 1000
  }
}));
JWTSessions
Stateless, scalableStateful, needs store (Redis)
Good for APIs/mobileGood for traditional web apps
Token in headerCookie-based

httpOnly Cookies vs localStorage

Problem with localStorage: Any XSS can steal token.

Better: Store JWT in httpOnly, Secure, SameSite cookie — JS cannot read it.

res.cookie('token', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict',
  maxAge: 7 * 24 * 60 * 60 * 1000
});

CORS

Browsers block cross-origin requests unless server allows.

const cors = require('cors');

app.use(cors({
  origin: process.env.CLIENT_URL, // not '*' with credentials
  credentials: true
}));

Interview answer: CORS is a browser security feature. Postman works without CORS because it’s not a browser enforcing same-origin policy.


Input Validation

Never trust client input.

const { body, validationResult } = require('express-validator');

app.post('/api/register',
  body('email').isEmail().normalizeEmail(),
  body('password').isLength({ min: 8 }),
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    // proceed
  }
);

Or with Zod:

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8)
});

const result = schema.safeParse(req.body);
if (!result.success) return res.status(400).json(result.error);

Security Headers (Helmet)

const helmet = require('helmet');
app.use(helmet());

Sets X-Content-Type-Options, X-Frame-Options, etc.


Rate Limiting

Prevent brute force on login:

const rateLimit = require('express-rate-limit');

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  message: { error: 'Too many attempts' }
});

app.post('/api/login', loginLimiter, loginHandler);

Environment Variables

// .env (NEVER commit)
JWT_SECRET=long-random-string
DATABASE_URL=mongodb://...
PORT=5000

// app.js
require('dotenv').config();
const secret = process.env.JWT_SECRET;
if (!secret) throw new Error('JWT_SECRET required');

Authorization vs Authentication

  • Authentication: Who are you? (login)
  • Authorization: What can you do? (roles, permissions)
function requireRole(role) {
  return (req, res, next) => {
    if (req.user.role !== role) {
      return res.status(403).json({ error: 'Forbidden' });
    }
    next();
  };
}

app.delete('/api/users/:id', authMiddleware, requireRole('admin'), deleteUser);

Common Vulnerabilities to Mention

ThreatPrevention
XSSSanitize output, CSP, httpOnly cookies
SQL/NoSQL injectionParameterized queries, validate input
CSRFSameSite cookies, CSRF tokens for sessions
Brute forceRate limiting, account lockout
Secret leak.env, gitignore, rotate keys

Interview Sound Bites

  • “Passwords hashed with bcrypt, never stored plain”
  • “JWT verified on every protected request”
  • “Validate input on server even if client validates”
  • “CORS configured for specific frontend origin”
  • “Secrets in environment variables, not code”