DEV Community

Ansh Sheladiya
Ansh Sheladiya

Posted on

Authentication in Node.js Applications: JWT, Password Hashing and Secure APIs

Authentication is one of the first security boundaries developers build when creating a Node.js application. Whether you are building a REST API, SaaS platform, mobile backend, or internal dashboard, you need a reliable way to verify who is making a request.

Node.js gives us the building blocks to implement authentication, but secure authentication requires more than checking a username and password. Password hashing, token validation, expiration, input validation, and careful error handling all play important roles in protecting user accounts and application data.

Building Secure Authentication in Node.js

A common authentication flow starts when a user submits an email and password to a login endpoint. The server looks up the account, compares the supplied password with the stored password hash, and issues an authentication token when the credentials are valid. The original password should never be stored directly in the database.

For password storage, Node.js provides the built-in crypto module, which can derive a strong key from a password using scrypt. In production systems, dedicated password-hashing libraries such as Argon2 or bcrypt are also common choices. The important principle is that passwords must be salted and computationally expensive to hash so that leaked password databases are harder to exploit.

JWTs are another common approach for API authentication. After successful login, the server signs a token containing limited claims such as the user's identifier and expiration time. Protected routes then verify the token before allowing access, while the server should reject malformed, expired, or incorrectly signed tokens.

The following example demonstrates the core concepts using only Node.js built-in modules. It creates a small HTTP API with registration, login, and protected-profile endpoints, while keeping the example dependency-free and easy to run and inspect.

const http = require('http');
const crypto = require('crypto');

const PORT = 3000;
const JWT_SECRET = 'replace-this-with-a-long-random-secret';
const users = new Map();

console.log('[1] Starting Node.js authentication example...');

function sendJson(res, statusCode, data) {
  res.writeHead(statusCode, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify(data));
}

function readBody(req) {
  return new Promise((resolve, reject) => {
    let body = '';

    req.on('data', chunk => {
      body += chunk;
      if (body.length > 10000) reject(new Error('Request body too large'));
    });

    req.on('end', () => {
      try {
        resolve(body ? JSON.parse(body) : {});
      } catch (error) {
        reject(new Error('Invalid JSON'));
      }
    });

    req.on('error', reject);
  });
}

function hashPassword(password, salt = crypto.randomBytes(16).toString('hex')) {
  const hash = crypto.scryptSync(password, salt, 64).toString('hex');
  return { salt, hash };
}

function verifyPassword(password, salt, storedHash) {
  const derivedHash = crypto.scryptSync(password, salt, 64);
  const originalHash = Buffer.from(storedHash, 'hex');
  return crypto.timingSafeEqual(derivedHash, originalHash);
}

function base64Url(value) {
  return Buffer.from(value)
    .toString('base64')
    .replace(/=/g, '')
    .replace(/\\+/g, '-')
    .replace(/\\//g, '_');
}

function createToken(payload) {
  const header = base64Url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
  const body = base64Url(JSON.stringify({ ...payload, exp: Math.floor(Date.now() / 1000) + 3600 }));
  const unsignedToken = `${header}.${body}`;
  const signature = crypto.createHmac('sha256', JWT_SECRET).update(unsignedToken).digest('base64url');
  return `${unsignedToken}.${signature}`;
}

function verifyToken(token) {
  const parts = token.split('.');
  if (parts.length !== 3) throw new Error('Invalid token format');

  const [header, payload, signature] = parts;
  const unsignedToken = `${header}.${payload}`;
  const expectedSignature = crypto.createHmac('sha256', JWT_SECRET).update(unsignedToken).digest('base64url');

  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
    throw new Error('Invalid token signature');
  }

  const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
  if (decoded.exp <= Math.floor(Date.now() / 1000)) throw new Error('Token expired');

  return decoded;
}

function getBearerToken(req) {
  const authorization = req.headers.authorization || '';
  if (!authorization.startsWith('Bearer ')) return null;
  return authorization.slice(7);
}

const server = http.createServer(async (req, res) => {
  console.log(`[REQUEST] ${req.method} ${req.url}`);

  try {
    if (req.method === 'POST' && req.url === '/register') {
      console.log('[2] Processing registration request...');
      const { email, password } = await readBody(req);

      if (!email || !password || password.length < 8) {
        return sendJson(res, 400, { error: 'Email and password of at least 8 characters are required' });
      }

      const normalizedEmail = email.toLowerCase().trim();
      if (users.has(normalizedEmail)) {
        return sendJson(res, 409, { error: 'User already exists' });
      }

      const credentials = hashPassword(password);
      users.set(normalizedEmail, { email: normalizedEmail, ...credentials });
      console.log(`[3] User registered: ${normalizedEmail}`);

      return sendJson(res, 201, { message: 'Registration successful' });
    }

    if (req.method === 'POST' && req.url === '/login') {
      console.log('[4] Processing login request...');
      const { email, password } = await readBody(req);
      const user = users.get((email || '').toLowerCase().trim());

      if (!user || !verifyPassword(password || '', user.salt, user.hash)) {
        console.log('[5] Login rejected: invalid credentials');
        return sendJson(res, 401, { error: 'Invalid credentials' });
      }

      const token = createToken({ sub: user.email });
      console.log('[6] Login successful and access token created');
      return sendJson(res, 200, { accessToken: token });
    }

    if (req.method === 'GET' && req.url === '/profile') {
      console.log('[7] Checking authentication token...');
      const token = getBearerToken(req);
      if (!token) return sendJson(res, 401, { error: 'Authentication required' });

      const session = verifyToken(token);
      const user = users.get(session.sub);
      if (!user) return sendJson(res, 401, { error: 'User no longer exists' });

      console.log(`[8] Access granted to ${user.email}`);
      return sendJson(res, 200, { user: { email: user.email } });
    }

    sendJson(res, 404, { error: 'Route not found' });
  } catch (error) {
    console.error('[ERROR]', error.message);
    sendJson(res, 500, { error: 'Internal server error' });
  }
});

server.listen(PORT, () => {
  console.log(`[9] Authentication API running at http://localhost:${PORT}`);
  console.log('[10] POST /register -> create an account');
  console.log('[11] POST /login -> receive an access token');
  console.log('[12] GET /profile -> access protected user data');
});
Enter fullscreen mode Exit fullscreen mode

Conclusion

Authentication should be treated as a security system rather than a simple login form. Hash passwords with a suitable password-hashing algorithm, keep signing secrets outside source control, validate incoming data, use HTTPS, and make tokens short-lived when appropriate.

The example demonstrates the fundamental mechanics, but production applications should add a persistent database, stronger secret management, rate limiting, account lockout or abuse protection, refresh-token handling where needed, structured validation, and secure cookie or authorization-header strategies based on the application architecture.

The biggest lesson is to keep authentication responsibilities isolated and predictable. Once registration, credential verification, token creation, and protected-route authorization are clearly separated, the same architecture can scale from a small Node.js API to a much larger application.

Top comments (0)