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

Mahad AhmedMahad Ahmed
5 min read

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.

0
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.