Introduction
Password-based authentication remains one of the most common and widely used methods to verify user identity in various online systems. It involves users providing a unique combination of a username and password to gain access to their accounts. Despite its prevalence, password-based authentication comes with security challenges, as weak or compromised passwords can lead to unauthorized access and data breaches.
In this blog, I will guide you exploring password-based authentication from an easy to medium level, implementing password hashing in a Node.js and TypeScript environment. By the end of this hands-on tutorial, you will have a better understanding of how Password-based authentication works in your applications.
Step 1: Setting Up the Node.js and TypeScript Environment
To get started, ensure you have Node.js installed on your machine. Create a new project folder and initialize it with a package.json file.
Here is the steps to show what I’ve done on Mac:
brew install npm httpie mkdir password-auth cd password-auth npm init -y npm install -g ts-node npm install body-parser bcryptjs express --save npm install @types/bcryptjs @types/express @types/body-parser --save
Setting up the programming environment is no doubt crucial, but let’s be honest, it can be a bit daunting. In my tutorials, I will try to make sure not to leave you hanging. I love providing comprehensive explanations, even for the simple tasks or commands. Let’s make this setup process a breeze together! I genuinely hope you find it helpful and that it keeps you smoothly sailing through the tutorial 🤓
Let’s walk through above commands.
▹ 1. brew
is the package manager for macOS or Linux. You can find the installation guide easily at their website. Here we used it to install npm
and httpie
. npm
is the JavaScript package manager for Node.js. We will test the server by using http
command provided by httpie
. The later one is a command-line HTTP client.
▹ 2. Then we created our project folder password-auth
.
▹ 3. npm init -y
is to instantly initialize a project. We avoid answering a bunch of questions with -y
.
▹ 4. When you want to use the commands provided by the package in your shell, on the command line or something, use npm
install it globally with -g
, so that its binaries end up in your PATH environment variable. In our case, we need ts-node
from command line.
▹ 5. ts-node
is a TypeScript execution engine for Node. js. It allows you to run your TypeScript code directly without precompiling your TypeScript code to JavaScript. Typically ts-node
transforms TypeScript to JavaScript in-memory without writing it to disk. You can find more deatils at here.
▹ 6. express
is a web framework for Node.js to build web application and APIs. Building a backend from-scratch for an application in Node.js can be tedious and time consuming. With express
, you can save time and focus on other important tasks.
▹ 7. If not installing @types/bcryptjs, @types/express, @types/body-parser
, you will hit below error when running your application:
npx stands for Node Package eXecute. It is simply an NPM package runner. NPX is installed automatically with NPM version 5.2. 0 and above.asdf
To enable TypeScript support on a Node.js backend API project, you need to set up TypeScript to compile your TypeScript code into JavaScript. Since TypeScript requires the types information for the package, we need to provide that. These @types packages offer type definitions for external modules that lack them. If you’re using an external package that already includes TypeScript definitions, you won’t need to install the corresponding @types package.
▹ 8. I don’t want to overcrowd this article with setup instructions, but this is the last point. You might have seen -save
and -save-dev
when using npm.
When using -save
, it will put the dependency into core dependency section of package.json, the dependencies
section. The other will put the dependencies to devDependencies
section. A core dependency is any package without which the application cannot perform its intended work. Example: express, body-parser etc.
Now, here is the output of npm list
and npm list -g
FYI:
Step 2: Creating the Server
Create an app.ts
file and set up a basic Express server with routes for user registration and login.
import express from 'express'; import bodyParser from 'body-parser'; import bcrypt from 'bcryptjs'; const app = express(); const PORT = 3000; app.use(bodyParser.json()); interface User { id: number; username: string; password: string; } let users: User[] = []; app.post('/register', async (req, res) => { try { const { username, password } = req.body; const salt = await bcrypt.genSalt(10); const hashedPassword = await bcrypt.hash(password, salt); const newUser: User = { id: users.length + 1, username, password: hashedPassword, }; users.push(newUser); res.status(201).json({ message: 'User registered successfully!' }); } catch (error) { res.status(500).json({ error: 'Internal server error' }); } }); app.post('/login', async (req, res) => { try { const { username, password } = req.body; const user = users.find((user) => user.username === username); if (!user) { return res.status(404).json({ error: 'User not found' }); } const isPasswordValid = await bcrypt.compare(password, user.password); if (!isPasswordValid) { return res.status(401).json({ error: 'Invalid password' }); } res.json({ message: 'Login successful!' }); } catch (error) { res.status(500).json({ error: 'Internal server error' }); } }); app.listen(PORT, () => { console.log(`Server is running on http://localhost:${PORT}`); });
Step 3: Explain the code
In this example, we are using an in-memory array to store registered users. In a real-world scenario, you would typically use a database for this purpose.
The /register
route handles user registration. When a user sends a POST request with their desired username
and password
, the server will hash the password using bcrypt and store the new user in the users
array.
The /login
route handles user login. When a user sends a POST request with their username
and password
, the server will find the corresponding user in the users
array, and then use bcrypt to compare the hashed password with the provided password.
⚠ You might have noticed, why above
bcrypt.compare
can compare the password without salt?
The reason is the salt has been stored as part of the hashed password. When you hash a password using bcrypt, the resulting hash contains both the salt and the password’s cryptographic hash.
For example, given plain passwordtestpassword
, the hashed password would be$2a$10$34rHf5RmJx1TZmZ7FM5BYe0BPXuw1bs6rYzzqyM7IXgN/VGcQmVMu
.
So in the above hashed password, there are three fields delimited by $ symbol.I) First part $2a$ identifies the Bcrypt algorithm version used. BCrypt was designed by the OpenBSD people. It was designed to hash passwords for storage in the OpenBSD password file. Hashed passwords are stored with a prefix to identify the algorithm used. BCrypt got the prefix
$2$
. So, besides$2a$
, there are$2x$
,$2y$
and$2b$
for BCrypt.II) Second part
$10$
10 is the cost factor (nothing but the salt rounds used while creating the salt string) If we do 15 rounds, then the value will be$15$
.III) Third part is the first 22 characters which are the salt string. In this case it is
34rHf5RmJx1TZmZ7FM5BYe . The remaining 31 characters are the hashed password.
In short, wikipedia gives this formula of bcrypt hashed password:
$2<a/b/x/y>$[cost]$[22 character salt][31 character hash]
The remaining string is the hashed password — Zqlv9ENS7zlIbkMvCSDIv7aup3WNH9W
So basically, the saltedHash = salt string + hashedPassword to protect from rainbow table attacks.
Step 4: Testing the Server
Now that the server is set up, I am going to test it using a tool called httpied
. Make sure you have installed it in previous steps.
First we need to launch the server from command line (make sure you’re already in password-auth
folder on command line):
npx ts-node ./app.ts
Open another terminal, testing register by http
:
echo '{"username": "testuser", "password": "testpassword"}' | http POST http://localhost:3000/register
Then it have created a user testuser
in the server with password testpassword
.
Next, we will test login:
echo '{"username": "testuser", "password": "testpassword"}' | http POST http://localhost:3000/login
We can also try a test with wrong password or wrong user name:
✎ Make sure to send requests with the appropriate JSON data to the appropriate endpoints (
/register
and/login
).
Summary
In this blog, we explored password-based authentication in Node.js and TypeScript using bcrypt for secure password hashing. By understanding the fundamentals of bcrypt and its automatic management of salts, we created a simple authentication mechanism that stores and compares hashed passwords securely.
We learned how to set up a Node.js and TypeScript environment on macOS, implemented an Express server with routes for user registration and login, and utilized bcrypt to securely hash and compare passwords.
The source code of this tutorial has been uploaded to GeekCoding101 github repo as well, feel free to take a look.
In the next blogs, we will continue building upon this foundation of secure authentication. We will explore more authentication methods, such as Basic Authentication, Two-Factor Authentication (2FA), and token-based authentication using JSON Web Tokens (JWT) and so on.
As we proceed, feel free to ask questions and provide feedback. I am here to support your journey towards building secure and reliable authentication solutions. So, stay tuned for the upcoming blogs 🎉🎉🎉