DEV Community

M DISHA SHETTY
M DISHA SHETTY

Posted on

Making Your Express.js API Secure: Authentication Middleware & Token Management

Image description
Link
Why Authentication Matters

Imagine leaving your house unlocked—anyone could walk in and take what they want. APIs work the same way. Without proper authentication, attackers can gain access to sensitive data or manipulate your system. JWTs and API keys provide a structured way to ensure only authorized users can use your API securely.

How Authentication Middleware Works
_
Our middleware handles two types of authentication:_

API Keys: If an API key is included in the request, it’s checked against a stored value.

JWTs: If no API key is found, the system looks for a JWT token in the Authorization header, validates it, and checks the user's permissions.

Here’s the authMiddleware.js in action:

import Configs from '../configs/config.js';
import { verifyAccessToken } from '../helpers/tokenManager.js';
import errorHandler from '../helpers/errorHandler.js';

export default function authMiddleware(requiredRoles = []) {
  return async (req, res, next) => {
    try {
      const authHeader = req.headers.authorization;
      const apiKeyHeader = req.headers['x-api-key'];

      if (apiKeyHeader) {
        if (!Configs?.X_API_KEY_DEV) {
          throw new Error('NOT_FOUND X_API_KEY_DEV is not set in environment');
        }

        if (apiKeyHeader !== Configs?.X_API_KEY_DEV) {
          throw new Error('FORBIDDEN Invalid or missing x-api-key');
        }
        return next();
      }

      if (!authHeader || !authHeader.startsWith('Bearer ')) {
        throw new Error('UNAUTHORIZED Authentication token is missing or malformed.');
      }

      const token = authHeader.replace('Bearer ', '');
      const decoded = await verifyAccessToken(token);

      if (decoded.name === 'TokenExpiredError') {
        throw new Error('UNAUTHORIZED Token expired.');
      }

      if (requiredRoles.length && !requiredRoles.includes('XAPIKEY')) {
        const hasRole = requiredRoles.some(roleNeeded =>
          decoded.roles?.some(role => role.permissions?.includes(roleNeeded))
        );
        if (!hasRole) {
          throw new Error('FORBIDDEN You do not have the required permissions.');
        }
      }

      req.user_id = decoded.sub || null;
      return next();
    } catch (err) {
      const error = errorHandler(err);
      return res.status(error.code).json({ message: error.message, keyword: error.keyword });
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

How JWT Token Management Works

JWT tokens contain user details and are signed to ensure their validity. To verify a token, we:

Decode its header to get the kid (Key ID).

Fetch the public key from the JWKS endpoint.

Use the key to validate the token’s signature and confirm its authenticity.

Here’s how we handle it in tokenManager.js:

import fs from 'fs';
import path from 'path';
import axios from 'axios';
import jwt from 'jsonwebtoken';
import crypto from 'crypto';
import Configs from '../configs/config.js';

const keysDirPath = path.join(__dirname, '../../keys');
const baseUrl = `${Configs.JWKS_URI}?secret=${Configs.JWKS_SECRET}&kid=`;

export async function verifyAccessToken(token) {
  try {
    const decodedHeader = jwt.decode(token, { complete: true }).header;
    let publicKey = checkLocalKeyDirectory(decodedHeader.kid);
    if (!publicKey) {
      publicKey = await fetchAndSaveKey(decodedHeader.kid);
    }

    const verifyOptions = { issuer: Configs.TOKEN_ISSUER, algorithms: ['RS256'] };
    if (Configs.TOKEN_AUDIENCE) {
      verifyOptions.audience = Configs.TOKEN_AUDIENCE;
    }

    return jwt.verify(token, publicKey, verifyOptions);
  } catch (error) {
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

Best Practices for Secure Authentication

Keep Secrets Safe: Store API keys and JWT secrets in environment variables.

Handle Token Expiry: Implement token refresh to prevent expired tokens from causing issues.

Rate Limit Requests: Prevent API abuse by limiting request frequency per user.

Monitor Authentication Failures: Log failed login attempts for security analysis.

Use Role-Based Access Control (RBAC): Assign permissions based on user roles.

Top comments (0)