It's Simple to Build Your Own Email/Password Authentication REST API (Express.js)
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.
Search for a command to run...
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.
No comments yet. Be the first to comment.
Introduction Hosting your email server is the first step to secure your privacy It saves you money if you own multiple domains What You'll Need [ ] Brand new VPS or any Linux Server (My recommended VPS provider is Linode) [ ] Ubuntu 22.04 LTS i...
Introduction AWS has a MySQL compatible database but it's expensive and sometimes the cost doesn't justify the convenience we get from having such a service for personal projects or projects that don't make money. The alternative solution is to have ...
How to Setup EC2 for Dockerized Next.js Deployment Introduction Next.js is great serve-side oriented frontend framework that makes building SEO-friendly websites easier. Deploying it is trickier than it needs to be, I understand Vercel needs to make ...
Sometimes you need to allow for self-hosted subscription of your SaaS whether it's because your client has their own air-gapped system that is not connected to the internet or they want to host it in their on-premise servers. Having the option is alw...
I'm running windows 11 on a virtual machine to do few tests and I wanted to change the theme but in order to do that you need to activate your copy of Windows. I don't think it's feasible to buy a license to test few things in a virtual machine. If y...
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.
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
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!' });
}
}
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' });
}
};
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}`);
});
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.