40+ Authentication & JWT Interview Questions 2025: Sessions, OAuth & Security

·18 min read
authenticationjwtinterview-questionsnodejsoauthsecuritybackend

"How would you implement authentication?" This question appears in virtually every backend interview, yet it catches candidates off guard. The answer isn't just "use JWT" or "use Passport.js"—interviewers want to see that you understand the tradeoffs between different approaches and can choose the right one for specific requirements.

This guide covers the essential authentication questions you'll face in Node.js backend interviews, from basic concepts to advanced security patterns.

Table of Contents

  1. Authentication Fundamentals Questions
  2. Session-Based Authentication Questions
  3. JWT Authentication Questions
  4. OAuth 2.0 Questions
  5. Password Security Questions
  6. Multi-Factor Authentication Questions
  7. Security Best Practices Questions

Authentication Fundamentals Questions

These foundational questions test your understanding of core authentication concepts.

What is the difference between authentication and authorization?

Authentication verifies WHO you are—proving your identity through credentials like passwords, tokens, or biometrics. Authorization determines WHAT you can do—checking if the authenticated user has permission to access a resource or perform an action.

Authentication always comes first. You must know who someone is before deciding what they're allowed to do. A 401 status code means "I don't know who you are" while 403 means "I know who you are, but you're not allowed."

flowchart TB
    subgraph authn["Authentication: WHO are you?"]
        A1["Verify identity"]
        A2["Check credentials<br/>(password, token, biometric)"]
        A3["Result: This is user #123"]
    end
 
    subgraph authz["Authorization: WHAT can you do?"]
        B1["Check permissions"]
        B2["Evaluate roles/policies"]
        B3["Result: User #123 can<br/>edit this resource"]
    end
 
    authn --> authz
// Express middleware example
const authenticate = async (req, res, next) => {
  // WHO is this?
  const token = req.cookies.token;
  const user = await verifyToken(token);
  if (!user) return res.status(401).json({ error: 'Not authenticated' });
  req.user = user;
  next();
};
 
const authorize = (...roles) => (req, res, next) => {
  // WHAT can they do?
  if (!roles.includes(req.user.role)) {
    return res.status(403).json({ error: 'Not authorized' });
  }
  next();
};
 
// Usage
app.delete('/users/:id',
  authenticate,           // Must be logged in
  authorize('admin'),     // Must be admin
  deleteUser
);

How would you implement authentication in a Node.js application?

The implementation depends on the use case. For a traditional web app, session-based auth with cookies is simple and supports immediate revocation. For an API serving mobile apps or SPAs, JWT with short-lived access tokens and refresh tokens works better. For "Login with Google" features, OAuth 2.0 is the standard approach.

In Node.js, Passport.js handles all these strategies, but for complex requirements like MFA, managed services like Auth0 can reduce implementation burden.

When should you use sessions versus JWT?

The choice depends on your architecture and requirements. Sessions store user state on the server with a session ID in a cookie. JWTs are self-contained tokens with encoded user data, signed by the server.

Sessions are simpler for traditional web apps and support immediate revocation, but require server-side storage and are harder to scale. JWTs are stateless and scale horizontally, but can't be revoked until expiry without a blacklist.

In practice, many applications combine approaches: sessions for the web interface and JWT for API access.

RequirementSessionJWT
Server-rendered web appYesPossible
REST API for mobilePossibleYes
MicroservicesComplexYes
Immediate revocation neededYesWith blacklist
Cross-domain authNoYes
Horizontal scalingWith RedisYes

Session-Based Authentication Questions

Sessions are the traditional approach, still widely used for server-rendered web apps.

How does session-based authentication work?

When a user logs in, the server validates their credentials, creates a session stored in a database or Redis, and sends a session ID in an httpOnly cookie. On subsequent requests, the browser automatically sends the cookie, and the server looks up the session to identify the user.

The key advantage is that the server has full control over sessions—you can immediately revoke access by deleting the session.

1. User submits credentials
2. Server validates, creates session in database/Redis
3. Server sends session ID in httpOnly cookie
4. Browser sends cookie with every request
5. Server looks up session, attaches user to request

How do you implement session authentication in Express?

Express-session with a Redis store provides production-ready session management. Configure the cookie with security options: httpOnly prevents JavaScript access, secure ensures HTTPS-only transmission, and sameSite provides CSRF protection.

const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const redis = require('redis');
const bcrypt = require('bcrypt');
 
const redisClient = redis.createClient();
 
app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production',  // HTTPS only
    httpOnly: true,      // No JavaScript access
    maxAge: 24 * 60 * 60 * 1000,  // 24 hours
    sameSite: 'strict'   // CSRF protection
  }
}));
 
// Login
app.post('/login', async (req, res) => {
  const { email, password } = req.body;
 
  const user = await User.findOne({ email });
  if (!user) {
    // Don't reveal if email exists
    return res.status(401).json({ error: 'Invalid credentials' });
  }
 
  const valid = await bcrypt.compare(password, user.passwordHash);
  if (!valid) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
 
  // Regenerate session to prevent fixation
  req.session.regenerate((err) => {
    req.session.userId = user.id;
    req.session.role = user.role;
    res.json({ message: 'Logged in' });
  });
});
 
// Logout
app.post('/logout', (req, res) => {
  req.session.destroy((err) => {
    res.clearCookie('connect.sid');
    res.json({ message: 'Logged out' });
  });
});
 
// Auth middleware
const requireAuth = (req, res, next) => {
  if (!req.session.userId) {
    return res.status(401).json({ error: 'Not authenticated' });
  }
  next();
};

What are the pros and cons of session-based authentication?

Sessions offer immediate revocation by simply deleting the session, small cookie size (around 32 bytes), and full server control over user state. However, they require server-side storage, are harder to scale horizontally without shared session stores like Redis, don't work across different domains, and need CSRF protection.

ProsCons
Immediate revocation (delete session)Requires server-side storage
Small cookie size (~32 bytes)Harder to scale (need shared session store)
Server has full controlDoesn't work across different domains
Simple to implementCSRF protection needed

JWT Authentication Questions

JWTs are the standard for stateless APIs, microservices, and cross-domain authentication.

What is a JWT and what does its structure look like?

A JSON Web Token (JWT) is a self-contained token that encodes user data and is cryptographically signed by the server. It consists of three parts separated by dots: a header specifying the algorithm, a payload containing claims (user data and metadata), and a signature that verifies the token hasn't been tampered with.

The server validates the signature without needing to look up state in a database, making JWTs stateless and horizontally scalable.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4iLCJpYXQiOjE1MTYyMzkwMjJ9.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

[Header].[Payload].[Signature]
// Header (algorithm + type)
{
  "alg": "HS256",
  "typ": "JWT"
}
 
// Payload (claims)
{
  "sub": "user123",        // Subject (user ID)
  "name": "John Doe",
  "role": "admin",
  "iat": 1516239022,       // Issued at
  "exp": 1516242622        // Expiration
}
 
// Signature
HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

How do you implement JWT authentication with refresh tokens?

The pattern uses two tokens: a short-lived access token (15 minutes) for API requests and a longer-lived refresh token (7 days) stored in an httpOnly cookie for obtaining new access tokens. This balances security (short access token lifetime) with user experience (no frequent re-login).

When the access token expires, the client calls the refresh endpoint to get a new one. Refresh tokens should be rotated on each use for additional security.

const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
 
const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET;
const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET;
const ACCESS_TOKEN_EXPIRY = '15m';
const REFRESH_TOKEN_EXPIRY = '7d';
 
// Generate tokens
const generateTokens = (user) => {
  const accessToken = jwt.sign(
    { userId: user.id, role: user.role },
    ACCESS_TOKEN_SECRET,
    { expiresIn: ACCESS_TOKEN_EXPIRY }
  );
 
  const refreshToken = jwt.sign(
    { userId: user.id, tokenVersion: user.tokenVersion },
    REFRESH_TOKEN_SECRET,
    { expiresIn: REFRESH_TOKEN_EXPIRY }
  );
 
  return { accessToken, refreshToken };
};
 
// Login
app.post('/login', async (req, res) => {
  const { email, password } = req.body;
 
  const user = await User.findOne({ email });
  if (!user || !await bcrypt.compare(password, user.passwordHash)) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
 
  const { accessToken, refreshToken } = generateTokens(user);
 
  // Store refresh token in httpOnly cookie
  res.cookie('refreshToken', refreshToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000  // 7 days
  });
 
  res.json({ accessToken });
});
 
// Refresh token endpoint
app.post('/refresh', async (req, res) => {
  const { refreshToken } = req.cookies;
  if (!refreshToken) {
    return res.status(401).json({ error: 'No refresh token' });
  }
 
  try {
    const payload = jwt.verify(refreshToken, REFRESH_TOKEN_SECRET);
    const user = await User.findById(payload.userId);
 
    // Check token version (for revocation)
    if (!user || user.tokenVersion !== payload.tokenVersion) {
      return res.status(401).json({ error: 'Invalid refresh token' });
    }
 
    const tokens = generateTokens(user);
 
    // Rotate refresh token
    res.cookie('refreshToken', tokens.refreshToken, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'strict',
      maxAge: 7 * 24 * 60 * 60 * 1000
    });
 
    res.json({ accessToken: tokens.accessToken });
  } catch (error) {
    return res.status(401).json({ error: 'Invalid refresh token' });
  }
});
 
// Auth middleware
const authenticateJWT = (req, res, next) => {
  const authHeader = req.headers.authorization;
 
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No token provided' });
  }
 
  const token = authHeader.split(' ')[1];
 
  try {
    const payload = jwt.verify(token, ACCESS_TOKEN_SECRET);
    req.user = payload;
    next();
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired' });
    }
    return res.status(401).json({ error: 'Invalid token' });
  }
};

How do you handle logout with JWT?

Since JWTs can't be invalidated before expiry by design, you need a revocation strategy. The most common approach uses a token version stored in the database. Each user has a tokenVersion field. When they log out, increment it. The refresh token includes the version at issue time—if it doesn't match the current version, it's rejected.

For immediate access token invalidation, use a short-lived blacklist in Redis that expires when the token would have expired anyway.

// Logout (revoke all refresh tokens for user)
app.post('/logout', authenticateJWT, async (req, res) => {
  // Increment token version to invalidate all refresh tokens
  await User.findByIdAndUpdate(req.user.userId, {
    $inc: { tokenVersion: 1 }
  });
 
  res.clearCookie('refreshToken');
  res.json({ message: 'Logged out' });
});

What are common JWT security pitfalls?

Several common mistakes can compromise JWT security. Storing tokens in localStorage exposes them to XSS attacks. Using the 'none' algorithm or weak secrets makes tokens forgeable. Long expiration times increase the window for token theft. Always specify allowed algorithms, use strong secrets (256+ bits), keep access tokens short-lived, and store sensitive tokens in httpOnly cookies.

// BAD: Storing in localStorage (XSS vulnerable)
localStorage.setItem('token', accessToken);
 
// GOOD: httpOnly cookie for refresh token
res.cookie('refreshToken', token, { httpOnly: true });
 
// BAD: Using 'none' algorithm
jwt.verify(token, secret, { algorithms: ['none', 'HS256'] });
 
// GOOD: Specify allowed algorithms
jwt.verify(token, secret, { algorithms: ['HS256'] });
 
// BAD: Weak secret
const secret = 'secret123';
 
// GOOD: Strong secret (256 bits for HS256)
const secret = crypto.randomBytes(32).toString('hex');
 
// BAD: Long expiration
{ expiresIn: '30d' }
 
// GOOD: Short access token, longer refresh token
accessToken: { expiresIn: '15m' }
refreshToken: { expiresIn: '7d' }

What are the pros and cons of JWT authentication?

JWTs are stateless with no server storage required, scale horizontally easily, work across domains and services, and contain user data to reduce database queries. However, they can't be revoked until expiry without maintaining a blacklist, are larger than session cookies, make token theft harder to detect, and can expose sensitive data if not properly handled.

ProsCons
Stateless - no server storageCan't revoke until expiry (without blacklist)
Scales horizontally easilyLarger than session cookies
Works across domains/servicesToken theft harder to detect
Contains user data (fewer DB queries)Sensitive data exposure if not encrypted

What happens if a JWT secret is compromised?

A compromised JWT secret is a critical incident. You must immediately rotate the secret and deploy the change. All existing tokens become invalid, forcing users to re-authenticate.

To minimize impact, use different secrets for access versus refresh tokens, so compromising one doesn't compromise both. Long-term, consider asymmetric keys (RS256) where only the private key needs protecting—the public key can be freely distributed for verification.


OAuth 2.0 Questions

OAuth 2.0 is the standard for delegated authorization and social login.

How does OAuth 2.0 work?

OAuth 2.0 is an authorization framework that lets users grant third-party apps limited access to their resources without sharing passwords. The typical flow redirects users to the authorization server (Google, GitHub), where they consent to specific permissions. The server returns an authorization code, which your backend exchanges for access tokens.

OAuth 2.0 is not authentication by itself—it's authorization. OpenID Connect adds the identity layer on top.

sequenceDiagram
    participant U as User Browser
    participant A as Your App (Backend)
    participant G as Google Auth Server
 
    U->>A: 1. Click "Login with Google"
    A->>G: 2. Redirect to Google
    G->>U: 3. User consents
    U->>A: 4. Redirect with auth code
    A->>G: 5. Exchange code for tokens
    G->>A: 6. Access + ID tokens
    A->>U: 7. Create session

How do you implement OAuth with Passport.js?

Passport.js provides a clean abstraction for OAuth strategies. The strategy handles the OAuth flow, and you implement the callback to find or create users based on their profile data from the provider.

const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
 
passport.use(new GoogleStrategy({
    clientID: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    callbackURL: '/auth/google/callback'
  },
  async (accessToken, refreshToken, profile, done) => {
    try {
      // Find or create user
      let user = await User.findOne({ googleId: profile.id });
 
      if (!user) {
        user = await User.create({
          googleId: profile.id,
          email: profile.emails[0].value,
          name: profile.displayName,
          avatar: profile.photos[0]?.value
        });
      }
 
      return done(null, user);
    } catch (error) {
      return done(error, null);
    }
  }
));
 
// Serialize user to session
passport.serializeUser((user, done) => {
  done(null, user.id);
});
 
passport.deserializeUser(async (id, done) => {
  const user = await User.findById(id);
  done(null, user);
});
 
// Routes
app.get('/auth/google',
  passport.authenticate('google', { scope: ['profile', 'email'] })
);
 
app.get('/auth/google/callback',
  passport.authenticate('google', { failureRedirect: '/login' }),
  (req, res) => {
    res.redirect('/dashboard');
  }
);

What are the different OAuth 2.0 grant types?

Different grant types suit different application architectures. Authorization Code is the most secure for web apps with backends because the client secret stays on the server. Authorization Code with PKCE is required for mobile apps and SPAs where the client secret can't be protected. Client Credentials is for server-to-server communication with no user involvement.

The Implicit and Password grant types are deprecated due to security concerns.

Grant TypeUse CaseSecurity Level
Authorization CodeWeb apps with backendHigh
Authorization Code + PKCESPAs, mobile appsHigh
Client CredentialsServer-to-serverHigh
Implicit (deprecated)Legacy SPAsLow - don't use
Password (deprecated)Trusted first-partyLow - avoid

Password Security Questions

Password handling is heavily tested in interviews.

How do you securely store passwords?

Never store passwords in plain text. Use bcrypt (or Argon2) with a sufficient work factor—12 rounds is a reasonable default. Bcrypt automatically handles salting, making rainbow table attacks ineffective.

The work factor should be high enough to make brute force attacks expensive while keeping login responsive (under 250ms is a good target).

const bcrypt = require('bcrypt');
 
// Hashing passwords
const SALT_ROUNDS = 12;  // Adjustable work factor
 
const hashPassword = async (password) => {
  return bcrypt.hash(password, SALT_ROUNDS);
};
 
const verifyPassword = async (password, hash) => {
  return bcrypt.compare(password, hash);
};

How do you implement password validation?

Password validation should check length, complexity requirements, and common password lists. However, NIST guidelines now recommend length over complexity—a long passphrase is more secure than a short complex password.

const validatePassword = (password) => {
  const errors = [];
 
  if (password.length < 8) errors.push('At least 8 characters');
  if (!/[A-Z]/.test(password)) errors.push('At least one uppercase letter');
  if (!/[a-z]/.test(password)) errors.push('At least one lowercase letter');
  if (!/[0-9]/.test(password)) errors.push('At least one number');
 
  // Check against common passwords
  const common = ['password', '12345678', 'qwerty'];
  if (common.includes(password.toLowerCase())) {
    errors.push('Password too common');
  }
 
  return errors;
};

How do you implement secure password reset?

Password reset requires generating a cryptographically random token, hashing it before storage (so database access doesn't reveal tokens), and expiring it quickly (1 hour maximum). The unhashed token goes to the user via email; you store only the hash.

const crypto = require('crypto');
 
const generateResetToken = async (user) => {
  const token = crypto.randomBytes(32).toString('hex');
  const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
 
  user.resetToken = hashedToken;
  user.resetTokenExpiry = Date.now() + 3600000; // 1 hour
  await user.save();
 
  return token; // Send this to user, store hashed version
};
 
const verifyResetToken = async (token) => {
  const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
 
  const user = await User.findOne({
    resetToken: hashedToken,
    resetTokenExpiry: { $gt: Date.now() }
  });
 
  return user;
};

Multi-Factor Authentication Questions

MFA adds critical security for sensitive applications.

How do you implement TOTP-based MFA?

Time-based One-Time Passwords (TOTP) work with authenticator apps like Google Authenticator. You generate a secret key, display it as a QR code for the user to scan, and verify codes by checking if they match the expected value for the current time window.

const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
 
// Setup MFA
app.post('/mfa/setup', authenticateJWT, async (req, res) => {
  const secret = speakeasy.generateSecret({
    name: `MyApp:${req.user.email}`
  });
 
  // Store secret temporarily until verified
  await User.findByIdAndUpdate(req.user.userId, {
    mfaSecretTemp: secret.base32
  });
 
  const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url);
  res.json({ qrCode: qrCodeUrl, secret: secret.base32 });
});
 
// Verify and enable MFA
app.post('/mfa/verify', authenticateJWT, async (req, res) => {
  const { token } = req.body;
  const user = await User.findById(req.user.userId);
 
  const verified = speakeasy.totp.verify({
    secret: user.mfaSecretTemp,
    encoding: 'base32',
    token,
    window: 1  // Allow 1 period tolerance
  });
 
  if (!verified) {
    return res.status(400).json({ error: 'Invalid code' });
  }
 
  // Enable MFA
  user.mfaSecret = user.mfaSecretTemp;
  user.mfaEnabled = true;
  user.mfaSecretTemp = undefined;
  await user.save();
 
  res.json({ message: 'MFA enabled' });
});

How do you handle login with MFA?

The login flow checks if MFA is enabled after validating the password. If enabled and no MFA token is provided, return a response indicating MFA is required. The client then prompts for the code and resubmits.

app.post('/login', async (req, res) => {
  const { email, password, mfaToken } = req.body;
 
  const user = await User.findOne({ email }).select('+passwordHash +mfaSecret');
  if (!user || !await bcrypt.compare(password, user.passwordHash)) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
 
  if (user.mfaEnabled) {
    if (!mfaToken) {
      return res.status(200).json({ requiresMfa: true });
    }
 
    const verified = speakeasy.totp.verify({
      secret: user.mfaSecret,
      encoding: 'base32',
      token: mfaToken,
      window: 1
    });
 
    if (!verified) {
      return res.status(401).json({ error: 'Invalid MFA code' });
    }
  }
 
  // Issue tokens
  const tokens = generateTokens(user);
  res.json({ accessToken: tokens.accessToken });
});

Security Best Practices Questions

These questions test your overall security awareness.

How do you implement rate limiting for authentication?

Rate limiting is essential for preventing brute-force attacks. Use stricter limits for authentication endpoints than general API endpoints. Count failures per email address to prevent attackers from trying many passwords against one account.

const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
 
// General API rate limiting
const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 minutes
  max: 100,
  message: { error: 'Too many requests, try again later' }
});
 
// Strict limiting for auth endpoints
const authLimiter = rateLimit({
  store: new RedisStore({ client: redisClient }),
  windowMs: 15 * 60 * 1000,
  max: 5,  // 5 attempts per 15 minutes
  skipSuccessfulRequests: true,  // Only count failures
  keyGenerator: (req) => req.body.email || req.ip,
  message: { error: 'Too many login attempts, try again in 15 minutes' }
});
 
app.use('/api/', apiLimiter);
app.use('/login', authLimiter);
app.use('/register', authLimiter);

How would you implement "Remember Me" functionality?

"Remember Me" uses a longer-lived refresh token. When the user checks the option, set the refresh token expiry to 30 days instead of the default 7 days. The access token stays short-lived (15 minutes) for security. On each visit, if the access token is expired but the refresh token is valid, silently refresh the access token.

How would you design authentication for a banking app?

Banking apps require defense in depth. Implement strong authentication with password plus MFA (TOTP or hardware keys). Use short sessions with 15-minute inactivity timeouts and 8-hour maximums. Bind sessions to IP and device fingerprint. Require step-up authentication for sensitive operations like transfers. Log all auth events and monitor for anomalies. Apply aggressive rate limiting on failed attempts. Use HTTPS only with HSTS and certificate pinning for mobile.


Quick Reference

ConceptImplementationSecurity Note
Password storagebcrypt with 12+ roundsNever plain text
Session IDRandom 128-bit, httpOnly cookieRegenerate on login
Access tokenJWT, 15min expiryNever in localStorage
Refresh tokenOpaque or JWT, httpOnly cookieRotate on use
Password resetRandom token, 1h expiry, single useHash before storing
MFATOTP (Google Auth) or WebAuthnRequire for sensitive ops

Ready to ace your interview?

Get 550+ interview questions with detailed answers in our comprehensive PDF guides.

View PDF Guides