How to implement secure token-based authorization in JavaScript?

Content verified by Anycode AI
July 21, 2024
In web applications these days, a lot of emphasis has to be placed on the safety and efficiency of user authentication and authorization. Traditional session-based approaches are given in by most cases of scalability and security concerns. On the other hand, token-based authentication offers a strong approach, more so by JSON Web Tokens, since it decouples user sessions from the server. It solves the problem of implementations of secure token-based authorization in JavaScript by providing a start-to-finish guide on setting up a server with Node.js and Express, handling user authentication, and protecting routes with JSON Web Tokens. This guide will walk developers through improving security and scalability in applications by ensuring that only authenticated users get access to protected resources.

Token-based Authorization Overview

Token-based authorization is a pretty nifty way to secure access to various resources by validating a token. It's a popular choice in web apps for managing user authentication and authorization. Let’s look at how to implement this using JavaScript, complete with code snippets for both the server and client sides.

Setting Up the Environment

  • Node.js: Make sure you have the latest version from the official site.
  • Express: A minimalist and flexible Node.js web app framework.
  • jsonwebtoken: For signing and verifying tokens.
  • bcrypt: For hashing passwords.

First up, let's set up a new project and install the necessary packages:

mkdir secure-token-auth
cd secure-token-auth
npm init -y
npm install express jsonwebtoken bcrypt

Server-side Implementation

Initialization and Configuration

Start with server.js.

const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');

const app = express();
const PORT = process.env.PORT || 3000;
const SECRET_KEY = 'your_secret_key';

app.use(express.json());

const users = []; // We're keeping things simple with in-memory storage. Use a real DB for production!

// Error-handling Middleware
app.use((err, req, res, next) => {
    res.status(err.status || 500).json({ error: { message: err.message } });
});

User Registration

Add a route for user registration that handles password hashing.

app.post('/register', async (req, res, next) => {
    try {
        const { username, password } = req.body;

        if (!(username && password)) {
            throw new Error('Username and Password are required');
        }

        const hashedPassword = await bcrypt.hash(password, 10);
        users.push({ username, password: hashedPassword });

        res.status(201).send('User Registered');
    } catch (err) {
        next(err);
    }
});

User Login and Token Generation

Create a route for logging in, which checks the password and issues a token.

app.post('/login', async (req, res, next) => {
    try {
        const { username, password } = req.body;
        const user = users.find(u => u.username === username);

        if (!user) {
            return res.status(404).send('User Not Found');
        }

        const isMatch = await bcrypt.compare(password, user.password);

        if (!isMatch) {
            return res.status(400).send('Invalid Credentials');
        }

        const token = jwt.sign({ username: user.username }, SECRET_KEY, { expiresIn: '1h' });

        res.json({ token });
    } catch (err) {
        next(err);
    }
});

Token Verification Middleware

Add middleware to verify tokens.

const authenticateToken = (req, res, next) => {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];

    if (token == null) {
        return res.status(401).send('Token Required');
    }

    jwt.verify(token, SECRET_KEY, (err, user) => {
        if (err) {
            return res.status(403).send('Invalid Token');
        }

        req.user = user;
        next();
    });
};

Protected Route

Create a route that can only be accessed with a valid token.

app.get('/protected', authenticateToken, (req, res) => {
    res.send(`Hello ${req.user.username}, you have accessed a protected route!`);
});

Start the Server

app.listen(PORT, () => {
    console.log(`Server is running on http://localhost:${PORT}`);
});

Client-side Implementation

Register User

Using the Fetch API, we can interact with our server to register a user.

async function register(username, password) {
    const response = await fetch('http://localhost:3000/register', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username, password })
    });

    if (!response.ok) {
        throw new Error('Registration Failed');
    }

    console.log('User Registered');
}

Login User and Get Token

async function login(username, password) {
    const response = await fetch('http://localhost:3000/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username, password })
    });

    if (!response.ok) {
        throw new Error('Login Failed');
    }

    const data = await response.json();
    localStorage.setItem('token', data.token);
    console.log('User Logged In', data.token);
}

Access Protected Resource

async function accessProtectedResource() {
    const token = localStorage.getItem('token');

    if (!token) {
        throw new Error('Authorization Token Not Found');
    }

    const response = await fetch('http://localhost:3000/protected', {
        headers: { 'Authorization': `Bearer ${token}` }
    });

    if (!response.ok) {
        throw new Error('Failed to Access Resource');
    }

    const data = await response.text();
    console.log(data);
}

Wrapping Up

This guide walks you through implementing a secure token-based authorization system using JavaScript. It covers both the server and client sides. On the server, we use Express to handle HTTP requests, bcrypt to hash passwords, and jsonwebtoken to manage tokens. On the client side, the Fetch API helps us interact with the server for registration, login, and protected resource access.

Keep in mind a couple of recommendations:

  • Swap the in-memory storage for a real database in production.
  • Never hard-code your secret keys in your files. Use environment variables instead.
  • Always use HTTPS to protect token transmission and consider refresh tokens for longer sessions.
Have any questions?
Our CEO and CTO are happy to
answer them personally.
Get Beta Access
Anubis Watal
CTO at Anycode
Alex Hudym
CEO at Anycode