Skip to content

Instantly share code, notes, and snippets.

@Jaysok
Last active June 28, 2022 14:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Jaysok/a24970cb142d2a503ea45fea8cf1843c to your computer and use it in GitHub Desktop.
Save Jaysok/a24970cb142d2a503ea45fea8cf1843c to your computer and use it in GitHub Desktop.

Node.js sign-up and sign-in(log-in) simplified

I wrote some simplified code for sign-up and sign-in. You can see whole source code at the bottom of this gist.

Sign-up

Assuming an id-password based login, the general flow of sign-up would look like this:

  1. Check that the user information is in the database with the user ID. Return if already exists.
  2. Create salt with randomBytes function.
  3. Pass the salt value and the user's raw password(password) to the hash function (here derivePassword).
  4. Insert the user data into the db and save hash and salt in association with user data.
  5. After user data creation is completed, proceed with the subsequent procedure(here postSignUp which only logs user data).
import { scrypt, randomBytes, randomUUID as uuid } from "node:crypto";

// [...]

async function signUp(userId, password) {
  const user = getUserByUserId(userId);

  if (user) {
    throw Error(`Cannot create a user with the userId: ${userId}`);
  }

  // The salt should be as unique as possible. It is recommended that a salt is random and at least 16 bytes long. 
  // See NIST SP 800-132(https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf) for details.
  const salt = randomBytes(saltSize).toString('hex');
  const hash = await derivePassword(password, salt);
  
  return postSignUp(createUser({ userId, hash, salt }));
}

// [...]

// Password derivation function
async function derivePassword(rawPassword, salt) {
  return new Promise((resolve, reject) => {
    scrypt(rawPassword, salt, keyLength, scryptOptions, (err, dk) => {
      if (err) {
        return reject(err);
      }
      return resolve(dk.toString("hex"));
    });
  });
}

// [...]

// A dummy function to denote a procedure after signup
async function postSignUp(user) {
  console.log(
    `SignUp Success, (userId/hash/salt): (${user.userId}/${user.hash}/${user.salt})`
  );
}

Sign-in

Login will flow in the following order:

  1. We need to get user data first. To be precise, we need to get the hash and salt associated with the user data.
  2. We need to verify that the hash value created with the rawPassword passed by the user equal to the hash value stored in the database.
  • In this case, the compare function is used to verify, and the following two values are compared to check whether the rawPassword is correct.
    • hash : hash stored in the database (a hash value created with the password provided by the user when signing up)
    • derivePassword(rawPassword, salt): A hash value created from the rawPassword provided by the user when logging in
  • If the two values do not match return
  1. After the login is completed, proceed with the subsequent procedure(here postSignIn which only logs user data).
async function signIn(userId, rawPassword) {
  const user = getUserByUserId(userId);

  if (!user) {
    throw Error(`Signin failed!`);
  }

  const { hash, salt } = user;

  const ok = await compare(rawPassword, hash, salt);
  if (!ok) {
    throw Error("Signin faield");
  }

  return postSignIn(user);
}

// [...]

// password verification function
async function compare(rawPassword, hash, salt) {
  return hash === (await derivePassword(rawPassword, salt));
}

// [...]

// A dummy function to denote a procedure after signin
async function postSignIn(user) {
  console.log(
    `SignIn Success, (userId/hash/salt): (${user.userId}/${user.hash}/${user.salt})`
  );
}

derivePassword

As you can see, scrypt is used for password generation. Tune parameters in scryptOptions as you need.

// see scrypt from node built-ins
import { scrypt, randomBytes, randomUUID as uuid } from "node:crypto";

const scryptOptions = {
  N: 8192, // Alias for cost. Only one of both may be specified. Default: 16384.
  r: 8, // Alias for blockSize. Only one of both may be specified. Default: 8.
  p: 1, // Alias for parallelization. Only one of both may be specified. Default: 1.
  maxmem: 32 * 1024 * 1024, // Memory upper bound. It is an error when (approximately) 128 * N * r > maxmem.
};

// [...]

async function derivePassword(rawPassword, salt) {
  return new Promise((resolve, reject) => {
    scrypt(rawPassword, salt, keyLength, scryptOptions, (err, dk) => {
      if (err) {
        return reject(err);
      }
      return resolve(dk.toString("hex"));
    });
  });
}

References

Source code

import { scrypt, randomBytes, randomUUID as uuid } from "node:crypto";

const saltSize = 32;
const keyLength = 64;

/**
 * Scrypt paramters
 *
 * @see https://nodejs.org/api/crypto.html#cryptoscryptpassword-salt-keylen-options-callback
 * @see http://www.tarsnap.com/scrypt/scrypt-slides.pdf
 * @see https://www.tarsnap.com/scrypt/scrypt.pdf
 */
const scryptOptions = {
  N: 8192, // Alias for cost. Only one of both may be specified. Default: 16384.
  r: 8, // Alias for blockSize. Only one of both may be specified. Default: 8.
  p: 1, // Alias for parallelization. Only one of both may be specified. Default: 1.
  maxmem: 32 * 1024 * 1024, // Memory upper bound. It is an error when (approximately) 128 * N * r > maxmem.
};

// Simplified version of database
const db = {};

/**
 * Simplified version of getting credential of a user
 */
function getUserByUserId(userId) {
  return Object.keys(db)
    .map((uuid) => db[uuid])
    .find((data) => data.userId === userId);
}

function createUser(data) {
  const id = uuid();
  db[id] = {
    id,
    ...data,
    createdAt: new Date(),
  };

  return db[id];
}

// A dummy function to denote a procedure after signup
async function postSignUp(user) {
  console.log(
    `SignUp Success, (userId/hash/salt): (${user.userId}/${user.hash}/${user.salt})`
  );
}

// A dummy function to denote a procedure after signin
async function postSignIn(user) {
  console.log(
    `SignIn Success, (userId/hash/salt): (${user.userId}/${user.hash}/${user.salt})`
  );
}

async function signUp(userId, password) {
  const user = getUserByUserId(userId);

  if (user) {
    throw Error(`Cannot create a user with the userId: ${userId}`);
  }

  // The salt should be as unique as possible. It is recommended that a salt is random and at least 16 bytes long.
  // See NIST SP 800-132(https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf) for details.
  const salt = randomBytes(saltSize).toString("hex");
  const hash = await derivePassword(password, salt);

  return postSignUp(createUser({ userId, hash, salt }));
}

async function signIn(userId, rawPassword) {
  const user = getUserByUserId(userId);

  if (!user) {
    throw Error(`Signin failed!`);
  }

  const { hash, salt } = user;

  const ok = await compare(rawPassword, hash, salt);
  if (!ok) {
    throw Error("Signin faield");
  }

  return postSignIn(user);
}

// password derivation function
async function derivePassword(rawPassword, salt) {
  return new Promise((resolve, reject) => {
    scrypt(rawPassword, salt, keyLength, scryptOptions, (err, dk) => {
      if (err) {
        return reject(err);
      }
      return resolve(dk.toString("hex"));
    });
  });
}

// password verification function
async function compare(rawPassword, hash, salt) {
  return hash === (await derivePassword(rawPassword, salt));
}

(async () => {
  await signUp("jaysok", "superpassword!");
  await signIn("jaysok", "superpassword!");
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment