DEV Community

Cover image for Node.js Backend Security: Password Hashing & JWT From Scratch (DIY)
Kinanee Samson
Kinanee Samson

Posted on

Node.js Backend Security: Password Hashing & JWT From Scratch (DIY)

Node.js Backend Security: Password Hashing & JWT From Scratch (DIY)Securing user data in Node.js backend applications is paramount, especially where authentication and authorization are critical.

This article focuses on symmetric encryption, demonstrating how to implement robust security measures for password hashing and JSON Web Tokens (JWTs) using Node.js's built-in crypto module, avoiding external libraries. This DIY approach offers deep understanding and control over your application's security.

Secure Password Hashing

Password hashing converts a password into an irreversible string, safeguarding it even if the database is compromised. A basic hash with createHash is insufficient, as it yields identical results for identical inputs, making it vulnerable to rainbow table attacks. Initially:

import { createHash } from "crypto"

const password = "Test1234"

const hash = (text: string) => createHash('sha256')
  .update(text)
  .digest('hex')

// console.log('hash', hash(password))
Enter fullscreen mode Exit fullscreen mode

To counter this, a "salt"—a unique, random string—is added to the password before hashing, ensuring distinct hashes even for identical passwords.

import { createHash, randomBytes } from "crypto"

const password = "Test1234"

const salt = randomBytes(16).toString('hex')

const hash = (text: string) => createHash('sha256')
  .update(text + salt)
  .digest('hex')

// console.log('salted hash', hash(password))
Enter fullscreen mode Exit fullscreen mode

For robust password hashing and verification, Node.js's pbkdf2Sync and timingSafeEqual are preferred. pbkdf2Sync is a computationally intensive key derivation function that makes brute-force attacks impractical through numerous iterations. timingSafeEqual prevents timing attacks by ensuring comparison operations take a constant amount of time, regardless of input values.

Hashing with PBKDF2:

import { randomBytes, pbkdf2Sync } from "crypto";

const hashPassword = (password: string) => {
  const salt = randomBytes(16).toString('hex')
  return pbkdf2Sync(
    password,
    salt,
    100000, // Iterations
    64,     // Key length
    'sha512'
  ).toString('hex') + `.${salt}`
}

// console.log("Hashed Password:", hashPassword("Test1234"))
Enter fullscreen mode Exit fullscreen mode

Verifying with PBKDF2 and timingSafeEqual:

import { pbkdf2Sync, timingSafeEqual } from "crypto";

const verifyPassword = (password: string, hashedPassword: string) => {
  const salt = hashedPassword.split(".")[1]
  const newHash = pbkdf2Sync(
    password,
    salt,
    100000,
    64,
    'sha512'
  ).toString('hex')

  return timingSafeEqual(
    Buffer.from(`${newHash}.${salt}`),
    Buffer.from(`${hashedPassword}`)
  )
}

// Example usage
// const myHashedPass = hashPassword("Test1234");
// console.log("Password match:", verifyPassword("Test1234", myHashedPass));
Enter fullscreen mode Exit fullscreen mode

Crafting JSON Web Tokens (JWT) From Scratch

JWTs are used to securely transmit information. A JWT comprises a header, Payload, and Signature. JWT Creation:

import { createHmac } from "crypto";

type Payload = Record<string, any>;
type Header = {
  alg: string;
  type: string;
};

// Function to create the signature part of the JWT
const createSignature = (header: string, secret: string, payload: string) =>
  createHmac('sha256', secret)
    .update(`${header}.${payload}`)
    .digest('base64url');

// Function to sign and create the full JWT
const sign = (secret: string, payload: Payload) => {
  const header: Header = {
    alg: "HS256", // Hashing algorithm
    type: "JWT"
  };

  // Encode Header and Payload to Base64URL
  const encodedHeader = Buffer.from(JSON.stringify(header)).toString('base64url');
  const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url');

  // Create the signature
  const signature = createSignature(encodedHeader, secret, encodedPayload);

  // Combine parts to form the JWT
  return `${encodedHeader}.${encodedPayload}.${signature}`;
};

// Example Usage
// const myJwt = sign("Test1234", { _id: "user123", role: "admin" });
// console.log("Generated JWT:", myJwt);

Enter fullscreen mode Exit fullscreen mode

Verifying and Decoding JWTs

Verifying a JWT involves re-creating its signature and comparing it to the token's original signature. If they match, the token is valid. Decoding the payload simply requires base64-decoding the payload segment. JWT Verification:

import { createHmac } from "crypto"; // Ensure createHmac is imported

// Function to verify a JWT
const verify = (token: string, secret: string): boolean => {
  const parts = token.split('.');
  if (parts.length !== 3) {
    throw new Error('Invalid token format');
  }

  const [encodedHeader, encodedPayload, encodedSignature] = parts;

  // Recreate the signature
  const newSignature = createHmac('sha256', secret)
    .update(`${encodedHeader}.${encodedPayload}`)
    .digest('base64url');

  // Compare the new signature with the provided signature
  if (newSignature !== encodedSignature) {
    throw new Error('Invalid signature');
  }

  // To decode the payload:
  // try {
  //   const decodedPayload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8'));
  //   console.log("Decoded Payload:", decodedPayload);
  // } catch (e) {
  //   console.error("Failed to decode payload:", e);
  // }

  return true; // Token is valid
};

// Example Usage with a generated JWT
// const myGeneratedJwt = sign("Test1234", { _id: "user123", role: "admin" });
// try {
//   console.log("JWT Verified:", verify(myGeneratedJwt, "Test1234"));
// } catch (e) {
//   console.error("JWT Verification failed:", e.message);
// }
Enter fullscreen mode Exit fullscreen mode

Building security features directly with Node.js's crypto module offers deep understanding and control over your application's data protection. This DIY approach provides valuable insights into symmetric encryption, crucial for any backend developer. Remember, security is an ongoing process, and continuous learning is key to safeguarding user data effectively.

Top comments (0)