Security & Auth Interview Questions
Security and authentication questions are where many Express interviews separate junior candidates from senior ones. Interviewers want to see that you understand the trade-offs between session and token auth, that you can name the common web attacks and their Express-level mitigations, and that you know how to handle passwords and CORS correctly. This page collects the most frequently asked questions with crisp, technically accurate answers and runnable Express code.
Sessions vs JWT: what’s the difference?
A common opening question. The honest answer is that they solve the same problem (proving who the user is on subsequent requests) with opposite trade-offs around state.
With server-side sessions, the server stores session data (in memory, Redis, or a database) and the client only holds an opaque session ID in a cookie. With JWT, the server signs a token containing claims and stores nothing; the token itself is the proof.
| Aspect | Sessions | JWT |
|---|---|---|
| Server state | Yes (store required) | Stateless |
| Revocation | Easy — delete the session | Hard — needs a denylist or short TTL |
| Scaling | Needs shared store (Redis) | Trivial across services |
| Payload visibility | Hidden (opaque ID) | Readable by anyone (base64, not encrypted) |
| Best for | Traditional web apps | APIs, microservices, mobile |
Common gotcha: JWTs are signed, not encrypted. Never put secrets or PII in the payload — anyone can decode it. Signing only guarantees integrity, not confidentiality.
import jwt from "jsonwebtoken";
function issueToken(user) {
return jwt.sign(
{ sub: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: "15m" }
);
}
function authenticate(req, res, next) {
const header = req.headers.authorization || "";
const token = header.startsWith("Bearer ") ? header.slice(7) : null;
if (!token) return res.status(401).json({ error: "Missing token" });
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch {
return res.status(401).json({ error: "Invalid or expired token" });
}
}
How do you store passwords securely?
Never store plaintext or fast hashes (MD5/SHA-256). Use a slow, salted, adaptive hash such as bcrypt, scrypt, or Argon2. The slowness is the point — it makes brute-forcing expensive. bcrypt auto-generates and embeds a per-user salt.
import bcrypt from "bcrypt";
const SALT_ROUNDS = 12;
async function register(req, res) {
const hash = await bcrypt.hash(req.body.password, SALT_ROUNDS);
await db.users.insert({ email: req.body.email, passwordHash: hash });
res.status(201).json({ message: "Created" });
}
async function login(req, res) {
const user = await db.users.findOne({ email: req.body.email });
const ok = user && (await bcrypt.compare(req.body.password, user.passwordHash));
if (!ok) return res.status(401).json({ error: "Invalid credentials" });
res.json({ token: issueToken(user) });
}
Tip: return the same generic “Invalid credentials” message whether the email is unknown or the password is wrong. Differentiating them leaks which accounts exist (user enumeration).
What is CSRF and how do you prevent it in Express?
Cross-Site Request Forgery tricks an authenticated user’s browser into sending an unwanted request to your site, relying on cookies being sent automatically. It only affects cookie-based auth — token-in-header APIs are immune because the browser won’t attach an Authorization header to a forged request.
Mitigations:
- Set cookies with
SameSite=LaxorStrict, plusHttpOnlyandSecure. - Use a CSRF token (double-submit or synchronizer pattern) for state-changing requests.
import session from "express-session";
import { doubleCsrf } from "csrf-csrf";
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { httpOnly: true, secure: true, sameSite: "lax" },
}));
const { doubleCsrfProtection, generateToken } = doubleCsrf({
getSecret: () => process.env.CSRF_SECRET,
});
app.get("/form", (req, res) => res.json({ csrfToken: generateToken(req, res) }));
app.post("/transfer", doubleCsrfProtection, (req, res) => res.json({ ok: true }));
What is XSS and how do you defend against it?
Cross-Site Scripting injects malicious JavaScript into pages your users view, letting attackers steal cookies or tokens. Defenses are mostly about output, not Express middleware:
- Escape/encode all user-controlled data when rendering HTML.
- Set a
Content-Security-Policyto restrict script sources. - Store auth tokens in
HttpOnlycookies so injected scripts can’t read them.
Use helmet to set safe security headers in one line:
import helmet from "helmet";
app.use(helmet()); // sets CSP, X-Content-Type-Options, HSTS, and more
How does CORS work and how do you configure it?
CORS (Cross-Origin Resource Sharing) is a browser mechanism that controls which origins may call your API from client-side JS. The server signals permission via Access-Control-Allow-* headers. Use the cors package and an explicit allowlist — never reflect arbitrary origins or use * together with credentials.
import cors from "cors";
const allowed = ["https://app.devcraftly.com"];
app.use(cors({
origin: (origin, cb) =>
!origin || allowed.includes(origin) ? cb(null, true) : cb(new Error("Not allowed")),
credentials: true,
}));
A successful preflight returns:
Output:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.devcraftly.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET,POST,PUT,DELETE
How do you implement authorization (not just authentication)?
Authentication answers “who are you?”; authorization answers “what may you do?”. Layer a role/permission check middleware after authentication:
const requireRole = (...roles) => (req, res, next) =>
roles.includes(req.user?.role)
? next()
: res.status(403).json({ error: "Forbidden" });
app.delete("/users/:id", authenticate, requireRole("admin"), deleteUser);
Best Practices
- Hash passwords with bcrypt/Argon2 and a high cost factor; never use plain SHA or MD5.
- Prefer
HttpOnly,Secure,SameSitecookies; keep tokens out oflocalStorage. - Apply
helmetand a strictContent-Security-Policyon every app. - Use short-lived access tokens with refresh tokens, and keep a revocation/denylist for JWTs.
- Validate and sanitize all input; rate-limit auth endpoints (
express-rate-limit) to slow brute force. - Enforce HTTPS, run
npm audit, and keep dependencies patched. - Return generic auth errors to avoid user enumeration, and log security events for auditing.