It's Simple to Build Your Own Email/Password Authentication REST API (Express.js)

This is based on a previous blog post using Golang, so anyone that is interested how to do this in Go, please check it here
Sometimes, building your own authentication REST API sounds scary but in this post, I'll show you how it's done using Node.js and Express.js. In this post you'll see how to build your own email/password authentication system.
Setting up your Node.js project
Let's initialize the express.js project
mkdir example-auth-api
cd example-auth-api
npm init -y
npm install express bcrypt jsonwebtoken body-parser
Setting up Express.js endpoints
Let's import all the required libraries
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const bodyParser = require('body-parser');
Then let's instantiate our express app.
const app = express();
app.use(bodyParser.json());
Let's add signUp
, login
as public routes and userProfile
protected endpoint.
app.post('/signup', signUp);
app.post('/login',login);
app.get('/users', authMiddleware, userProfile);
Now, let's create the signUp
endpoint handler.
const signUp = async (req, res) => {
try {
const { email, password } = req.body;
// check if a user with the current email exists
try {
await getUserByEmail(email);
return res.status(400).json({ error: 'User with this email already exists' });
} catch (err) {
// this means we can continue with signup
}
// hash the password
const hashedPassword = await bcrypt.hash(password, HASH_COST);
// save the user
const user = new User(Date.now(), email, hashedPassword);
await CreateUserInDb(user);
res.status(201).json({
message: 'User created successfully',
email: user.email
});
} catch (err) {
res.status(400).json({ error: 'Failed to create user' });
}
}
Now, let's create login
endpoint handler.
const login = async (req, res) => {
try {
const { email, password } = req.body;
// Get user by email
let user;
try {
user = await getUserByEmail(email);
} catch (err) {
return res.status(401).json({ error: 'Invalid email or password' });
}
// Verify password
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return res.status(401).json({ error: 'Invalid email or password' });
}
// Generate tokens
const { token, refreshToken } = generateToken(user);
res.status(200).json({
message: 'Login successful',
token,
refreshToken
});
} catch (err) {
res.status(401).json({ error: 'Invalid email or password' });
}
}
This endpoint verifies the user's credentials and generates a token. In production, use a library to create a secure JWT token and hashing passwords.
And finally, the userProfile
endpoint handler.
const userProfile = async (req, res) => {
try {
const userId = req.userId;
const user = await GetUserByID(userId);
res.json({ user });
} catch (err) {
res.status(401).json({ error: 'Access Denied!' });
}
}
Protecting Private Routes
In order to protect our private routes we need to use a middleware that keeps non-authenticated HTTP calls to our protected rourtes. As an example, we have 1 protected route that returns the current user's details. Note: it's not recommended to return the full details of the user, this might include sensitive data such as password hash or other unintended leaks.
const authMiddleware = async (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({ error: 'Authorization header missing' });
}
const tokenString = authHeader.replace('Bearer ', '');
try {
const token = jwt.verify(tokenString, JWT_SECRET);
if (!token.sub || token.typ !== 'authentication') {
return res.status(401).json({ error: 'Invalid token claims' });
}
req.userId = token.sub;
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
};
Complete example
Here is the complete example code:
// User model
class User {
constructor(id, email, password) {
this.id = id;
this.email = email;
this.password = password;
}
}
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
// Constants, please use .env to securely store instead of hardcoding it here.
const JWT_SECRET = 'your-secret-key';
const JWT_AUD_NAME = 'your-audience';
const JWT_ISSUER_NAME = 'your-issuer';
const TOKEN_EXPIRATION_DURATION = '1h';
const REFRESH_TOKEN_EXPIRATION_DURATION = '7d';
const HASH_COST = 10;
// TODO: create your implementation of the database access function
async function getUserByEmail(email) {
throw new Error('User not found');
}
async function getUserByID(id) {
throw new Error('User not found');
}
async function CreateUserInDb(user) {
return true;
}
// Middleware
const authMiddleware = async (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({ error: 'Authorization header missing' });
}
const tokenString = authHeader.replace('Bearer ', '');
try {
const token = jwt.verify(tokenString, JWT_SECRET);
if (!token.sub || token.typ !== 'authentication') {
return res.status(401).json({ error: 'Invalid token claims' });
}
req.userId = token.sub;
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
};
// Generate JWT tokens
function generateToken(user) {
const now = Math.floor(Date.now() / 1000);
const claims = {
sub: user.id,
typ: 'authentication',
aud: JWT_AUD_NAME,
exp: now + (60 * 60), // 1 hour
nbf: now,
iat: now,
iss: JWT_ISSUER_NAME
};
const token = jwt.sign(claims, JWT_SECRET);
claims.typ = 'refresh';
claims.exp = now + (7 * 24 * 60 * 60); // 7 days
const refreshToken = jwt.sign(claims, JWT_SECRET);
return { token, refreshToken };
}
// Routes
app.post('/signup', signUp);
app.post('/login',login);
app.get('/profile', authMiddleware, userProfile);
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Conclusion
This simple example shows how to build an email/password authentication REST API. You can expand this by integrating database and implementing getUserByEmail
and getUserByID
functions. With these building blocks, you're well on your way to create a fully functional authentication system.
Warning: Whatever you do, never try to build your own hashing function. You can also secure your API by using multiple layers of protection, such as OS-level firewall that only allows access to ports you want access from outside.
Please comment below if you're interested in expanding this to also add social authentication providers such as Google or Facebook, etc.
Subscribe to my newsletter
Read articles from Mahad Ahmed directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Mahad Ahmed
Mahad Ahmed
Mahad loves building mobile and web applications and is here to take you on a journey, filled with bad decisions and learning from mistakes, through this blog.