Introduction
The FIRST thing you do with any app is to create authentication for it, after all you will have users and they will need accounts!
In this article I will show you the basic idea with a standalone one file express server. We will use several popular off the shelf libraries such as express and express-session to hold the users authentication.
As this is a demo it’s not suitable for production so please do not use in a production app! See the last paragraph for things you’d have to do to make it production ready!
Setup (if already done then skip to ‘Start’ below)
Setting up a Node.js environment on Windows is a straightforward process. Here are the steps to get you started:
1. Download Node.js:
- Visit the official Node.js website: Node.js Downloads.
- Download the LTS (Long-Term Support) version and follow the installation wizard.
2. Verify Installation:
- Open a command prompt or PowerShell window.
- Check that Node.js and npm (Node Package Manager) are installed by running the following commands:
bash node -v npm -v
- These commands should print the installed Node.js and npm versions, confirming a successful installation.
3. Install a Code Editor (Optional):
- You can use any code editor or Integrated Development Environment (IDE) of your choice. Popular options include Visual Studio Code, Atom, or Sublime Text.
4. Create a Simple Node.js Application:
- Create a new file for your Node.js project named
index.js
and open it in your code editor.
Start – Express Server
Define our Dependencies
The first step is to define the libraries we need. Put this into the top of your index.js file. The comments give you a basic understanding of what each one does.
const express = require('express') //popular server framework
const session = require('express-session') //to store the session details on the clients browser (that get sent with every request)
const bodyParser = require('body-parser') //to 'decode' the contents of the request
const bcrypt = require('bcrypt') //enables hashed password storage and comparison. NEVER store plain text passwords!
const app = express() //creates an express server object
const port = 3000; //defines the port our app will use (localhost:3000 in this case)
Now open a terminal in VS Code (or whatever you use), navigate to your project folder and install the libraries by running:
npm i
Reading the Request Body – Middleware
When registration or login requests come in their data payloads will be basic strings. Use this line to intercept them and change to objects that our app can use much more easily.
app.use(bodyParser.urlencoded({ extended: true }));
Once this step is defined then you’ll be able to get the request data using “req.body” from anywhere within your app.
Setting a Session Object & Mock Database
Next, we tell our app to use a session object which defines the secret key used to encrypt user data. This is found in the “your-secret-key” in the below code. Feel free to change the key to your liking.
Note: You should never, ever store this key in your code (although I have, for simple demo purposes). In production you would probably store this key in environment variables!
Following the session setting we will store our registered user in a simple array called “users”. Again, this is just for dev / demo purposes. In production this data would be stored in a database.
//set teh session keys
app.use(session({ secret: 'your-secret-key', resave: true, saveUninitialized: true }));
// Sample in-memory database for storing users
const users = [];
Start the Server
Tell Express which port to serve your server:
app.listen(port, () => {
console.log(Server listening at http://localhost:${port});
});
Now save your file and run the server:
node index.js
Your server should start and you get a message in the console telling you which port the server is listening on.
From now on you’ll need to restart your server after each code change. Do this with Control + C to stop the current server and then rerunning node index.js.
Alternatively, you can download a library that watches your files for changes and auto reloads for you (such as pm2 or nodemon). I use pm2 for my production servers but both libraries work well.
Setup an HTML Template for Registration and Login
For now our server doesn’t actually serve anything useful. Let’s set up 2 routes that return registration and login forms:
- The /register endpoint is a “get” route that returns an HTML template for the user to register.
- The /login endpoint is also a “get” route for that new user to login.
The following code is VERY basic HTML and consists of a simple form with email and password inputs. In production you’d never have something this basic – you’d at least want to validate the email address and force a minimum password length / complexity.
// app.get defines the 'GET' request - which is what your browser sends when you type an address into the bar. The '/register' part tells this block to deliver the HTML if the user goes to 'yoursite.com/register'.
app.get('/register', (req, res) => {
// res.send responds to the request with the HTML form content
res.send(`
<form method="POST" action="/register">
<label for="username">Email:</label>
<input type="text" id="email" name="email"><br>
<label for="password">Password:</label>
<input type="password" id="password" name="password"><br>
<input type="submit" value="Register">
</form>
`);
});
app.get('/login', (req, res) => {
res.send(`
<form method="POST" action="/login">
<label for="username">Email:</label>
<input type="text" id="email" name="email"><br>
<label for="password">Password:</label>
<input type="password" id="password" name="password"><br>
<input type="submit" value="Login">
</form>
`);
});
Registering the User and Storing Serverside
The HTML endpoints we’ve defined do nothing right now, as we need to setup code to receive and process the data.
Remember the req.body mentioned earlier? That’s what carries the users email and password when they submit the registration form.
Our server will check if that user is already registered and if not then we proceed to create a new user.
IMPORTANT NOTE: We never store passwords in plain text on a server – no matter how secure you think it is! We always ‘hash’ them (put them through a one way algorithm, using bcrypt in this case) and then store the resulting hash.
Finally we store the user in our in-memory database. In production you would use a database such as mysql, Postgres or mongo. However, databases are beyond the scope of this article.
app.post('/register', async (req, res) => {
// get submitted form data from request body
const { email, password } = req.body;
// Check if the email is already registered
if (users.find(user => user.email === email)) {
return res.status(400).send('Email already registered');
}
// Hash the password before storing it
const hashedPassword = await bcrypt.hash(password, 10);
// Store the user in the in-memory database (you might want to use a database in a real-world application)
users.push({ email, password: hashedPassword });
console.log(users)
res.send('Registration successful');
});
You’re all set to navigate to /register. Go ahead and register an email and password and you should see the users array printed out to your console, complete with hashed password! For example:
[
{
email: '[email protected]',
password: '$2b$10$sSAw4PnBbbC1JJAs0sjj3ugD3MbEgrFk/y2spA1Ki6dOPH.eesTCK'
}
]
Note: The password hash is ultra secure and cannot be decrypted within a billion years with all the computing power in the world. That’s the power of maths and encryption 😉
On to the next step!
Checking Login Details
Now, if you navigate to the ‘/login’ page you are presented with a form to login – which does nothing! Let’s create the backend code in the /login endpoint that allows this to actually do something.
We will use the bcrypt compare function to see if the user password matches our database hash.
app.post('/login', async (req, res) => {
const { email, password } = req.body;
// Find the user by email
const user = users.find(user => user.email === email);
// Check if the user exists
if (!user) {
console.log("User doesn't exist")
return res.status(401).send('Invalid email or password');
}
// Compare the provided password with the hashed password in the database
const passwordMatch = await bcrypt.compare(password, user.password);
if (passwordMatch) {
// Set a session variable to indicate that the user is authenticated
req.session.authenticated = true;
res.send('Login successful');
} else {
res.status(401).send('Invalid email or password');
}
});
Restart the server, register a user and navigate to /login. You should be able to enter your details and receive back a message about having had a successful login.
Middleware to Protect User Area
Now that we have login credentials we need somewhere to send the user so they can see all their private stuff. We will create a /dashboard route that will only allow access to the user if logged in.
Now, seeing as we have to check the users credentials for each protected route we should create what’s called a middleware. This is an express.js feature that executes a function between when the route is called and when it returns some data.
Middlewares are extremely useful and you will use them everywhere in express JS! Let’s create a ‘requireAuth’ middleware that will perform authentication:
// Middleware to protect routes that require authentication
const requireAuth = (req, res, next) => {
if (req.session.authenticated) {
next(); // tells express to perform the next function, which is calling the dashboard in this case
} else {
res.status(401).send('Unauthorized');
}
};
By itself this middleware does nothing and needs to be called somewhere. Create a /dashboard endpoint, noting that the second parameter is the middleware we just created.
// Example of a protected route
app.get('/dashboard', requireAuth, (req, res) => {
res.send('Welcome to the dashboard!');
});
Save your file and restart your server.
Navigate to /dashboard and you’ll see that you are unauthorised! Go through the registration -> login -> dashboard loop to see that it all works now!
Optional – Niceties
It’s a bit annoying to manually navigate to pages so let’s make a simple link page at the root of our site:
app.get('', (req, res) => {
res.send(`
<a href="/register">Register</a><br>
<a href="/login">Log In</a><br>
<a href="/dashboard">Dashboard</a>
`)
})
Restart server and now when you navigate to localhost:3000 you will get a page with those links.
In my code linked below I’ve also changed the registration ‘success’ message to instead tell the browser to redirect to /login. I’ve also made a redirect for the login success case which takes the user to their dashboard.
Full Code – Express JS Session Server
const express = require('express');
const session = require('express-session');
const bodyParser = require('body-parser');
const bcrypt = require('bcrypt');
const app = express();
const port = 3000;
app.use(bodyParser.urlencoded({ extended: true }));
app.use(session({ secret: '87fir87r8f', resave: true, saveUninitialized: true }));
app.get('', (req, res) => {
res.send(`
<a href="/register">Register</a><br>
<a href="/login">Log In</a><br>
<a href="/dashboard">Dashboard</a>
`)
})
app.get('/register', (req, res) => {
res.send(`
<form method="POST" action="/register">
<label for="username">Email:</label>
<input type="text" id="email" name="email"><br>
<label for="password">Password:</label>
<input type="password" id="password" name="password"><br>
<input type="submit" value="Register">
</form>
`);
});
app.get('/login', (req, res) => {
res.send(`
<form method="POST" action="/login">
<label for="username">Email:</label>
<input type="text" id="email" name="email"><br>
<label for="password">Password:</label>
<input type="password" id="password" name="password"><br>
<input type="submit" value="Login">
</form>
`);
});
// Sample in-memory database for storing users
const users = [];
app.post('/register', async (req, res) => {
const { email, password } = req.body;
// Check if the email is already registered
if (users.find(user => user.email === email)) {
return res.status(400).send('Email already registered');
}
// Hash the password before storing it
const hashedPassword = await bcrypt.hash(password, 10);
// Store the user in the in-memory database (you might want to use a database in a real-world application)
users.push({ email, password: hashedPassword });
console.log(users)
res.redirect('/login');
});
app.post('/login', async (req, res) => {
const { email, password } = req.body;
// Find the user by email
const user = users.find(user => user.email === email);
// Check if the user exists
if (!user) {
console.log("User doesn't exist")
return res.status(401).send('Invalid email or password');
}
// Compare the provided password with the hashed password in the database
const passwordMatch = await bcrypt.compare(password, user.password);
if (passwordMatch) {
// Set a session variable to indicate that the user is authenticated
req.session.authenticated = true;
res.redirect('Dashboard');
} else {
console.log("User password doesn't match")
res.status(401).send('Invalid email or password');
}
});
// Middleware to protect routes that require authentication
const requireAuth = (req, res, next) => {
console.log(req.session)
if (req.session.authenticated) {
next();
} else {
res.status(401).send('Unauthorized');
}
};
// Example of a protected route
app.get('/dashboard', requireAuth, (req, res) => {
res.send('Welcome to the dashboard!');
});
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
});
Finally, if you have any questions or issues then please say so in the comments!
If you use replit then you can also access this server below. Note that replit has some ‘issues’ around node js so you may see some weird redirect loop behaviour.