DEV Community

Tooleroid
Tooleroid

Posted on

JWT Authentication in Node.js and TypeScript: Modern Web Development Guide

JSON Web Tokens (JWTs) have become the cornerstone of modern web authentication, especially in Node.js applications. They offer a stateless, scalable solution for handling user authentication and authorization. In this comprehensive guide, we'll explore how to implement JWT authentication in a Node.js and TypeScript environment, focusing on security best practices and real-world scenarios.

Understanding JWT Authentication

Before diving into the implementation, it's crucial to understand why JWTs are preferred in modern web applications:

  1. Stateless Authentication: Unlike traditional session-based authentication, JWTs don't require server-side storage. Each token contains all the necessary information about the user.

  2. Scalability: Since there's no need to store session information, you can easily scale your application across multiple servers.

  3. Cross-Domain Support: JWTs work seamlessly across different domains, making them perfect for microservices architectures.

  4. Security: When implemented correctly, JWTs provide a secure way to transmit information between parties.

Modern JWT Implementation in TypeScript

TypeScript adds an extra layer of type safety to our JWT implementation, helping catch potential issues at compile time rather than runtime. Let's explore how to structure a robust JWT authentication system.

Core Types and Interfaces

First, let's define our type system. These types will form the foundation of our JWT implementation:

// Essential JWT types
interface JWTPayload {
  sub: string;          // Subject (user ID)
  email?: string;       // Optional email
  role: UserRole;       // User role
  iat: number;         // Issued at
  exp: number;         // Expiration time
}

enum UserRole {
  ADMIN = 'admin',
  USER = 'user',
  GUEST = 'guest'
}

interface TokenResponse {
  accessToken: string;
  refreshToken: string;
  expiresIn: number;
}
Enter fullscreen mode Exit fullscreen mode

These type definitions serve several important purposes:

  • They provide clear documentation of the JWT payload structure
  • They enable TypeScript's type checking capabilities
  • They make the code more maintainable and self-documenting
  • They help prevent common mistakes when handling JWT data

Express Middleware Setup

The middleware layer is where JWT verification happens. This is a critical security checkpoint in your application:

import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';

interface AuthRequest extends Request {
  user?: JWTPayload;
}

const authMiddleware = async (
  req: AuthRequest,
  res: Response,
  next: NextFunction
): Promise<void> => {
  try {
    const token = req.headers.authorization?.split(' ')[1];
    if (!token) {
      throw new Error('No token provided');
    }

    const decoded = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload;
    req.user = decoded;
    next();
  } catch (error) {
    res.status(401).json({
      error: 'Authentication failed',
      message: error instanceof Error ? error.message : 'Unknown error'
    });
  }
};
Enter fullscreen mode Exit fullscreen mode

This middleware implementation includes several important security features:

  • Token extraction from the Authorization header
  • Proper error handling for missing or invalid tokens
  • Type-safe user information attachment to the request object
  • Clear error messages for debugging and client feedback

Token Management

Proper token management is crucial for maintaining security while providing a good user experience.

Token Generation

When generating tokens, we need to consider several factors:

class TokenService {
  private static readonly JWT_SECRET = process.env.JWT_SECRET!;
  private static readonly REFRESH_SECRET = process.env.JWT_REFRESH_SECRET!;

  static generateAccessToken(user: User): string {
    const payload: JWTPayload = {
      sub: user.id,
      email: user.email,
      role: user.role,
      iat: Math.floor(Date.now() / 1000),
      exp: Math.floor(Date.now() / 1000) + (60 * 60) // 1 hour
    };

    return jwt.sign(payload, this.JWT_SECRET);
  }

  static generateRefreshToken(userId: string): string {
    return jwt.sign(
      { sub: userId },
      this.REFRESH_SECRET,
      { expiresIn: '7d' }
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Key considerations in token generation:

  • Use of environment variables for secrets
  • Proper payload structure with standard JWT claims
  • Reasonable token expiration times
  • Separation of access and refresh token logic

Token Refresh Implementation

The refresh token mechanism allows for longer sessions while maintaining security:

class TokenManager {
  static async refreshTokens(refreshToken: string): Promise<TokenResponse> {
    try {
      const decoded = jwt.verify(
        refreshToken,
        process.env.JWT_REFRESH_SECRET!
      ) as JWTPayload;

      const user = await UserService.findById(decoded.sub);
      if (!user) {
        throw new Error('User not found');
      }

      const accessToken = TokenService.generateAccessToken(user);
      const newRefreshToken = TokenService.generateRefreshToken(user.id);

      return {
        accessToken,
        refreshToken: newRefreshToken,
        expiresIn: 3600 // 1 hour
      };
    } catch (error) {
      throw new Error('Token refresh failed');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This refresh mechanism provides several benefits:

  • Shorter lived access tokens for better security
  • Seamless user experience with automatic token renewal
  • Ability to revoke user sessions when needed
  • Protection against token theft and replay attacks

Security Features

Security should never be an afterthought. Here are essential security measures for your JWT implementation:

Rate Limiting

Rate limiting is your first line of defense against brute force attacks:

import rateLimit from 'express-rate-limit';

const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // 5 attempts
  message: {
    error: 'Too many login attempts',
    message: 'Please try again later'
  }
});

app.use('/api/auth/login', authLimiter);
Enter fullscreen mode Exit fullscreen mode

Benefits of implementing rate limiting:

  • Protection against brute force attacks
  • Prevention of DoS attacks
  • Resource protection
  • Better user experience for legitimate users

Token Blacklisting

While JWTs are stateless, sometimes you need to invalidate tokens before they expire:

class TokenBlacklist {
  private static blacklist = new Set<string>();

  static async add(token: string): Promise<void> {
    this.blacklist.add(token);

    // Clean up expired tokens
    const decoded = jwt.decode(token) as JWTPayload;
    if (decoded.exp) {
      setTimeout(() => {
        this.blacklist.delete(token);
      }, (decoded.exp * 1000) - Date.now());
    }
  }

  static isBlacklisted(token: string): boolean {
    return this.blacklist.has(token);
  }
}
Enter fullscreen mode Exit fullscreen mode

Blacklisting considerations:

  • Memory-efficient storage
  • Automatic cleanup of expired tokens
  • Quick token validation
  • Protection against compromised tokens

Error Handling

class AuthError extends Error {
  constructor(
    public statusCode: number,
    message: string,
    public code: string
  ) {
    super(message);
    this.name = 'AuthError';
  }
}

const handleAuthError = (error: unknown): AuthError => {
  if (error instanceof jwt.TokenExpiredError) {
    return new AuthError(401, 'Token expired', 'TOKEN_EXPIRED');
  }
  if (error instanceof jwt.JsonWebTokenError) {
    return new AuthError(401, 'Invalid token', 'INVALID_TOKEN');
  }
  return new AuthError(500, 'Authentication failed', 'AUTH_FAILED');
};
Enter fullscreen mode Exit fullscreen mode

Integration Examples

Express Route Implementation

import express from 'express';

const router = express.Router();

router.post('/login', async (req: Request, res: Response) => {
  try {
    const { email, password } = req.body;
    const user = await UserService.authenticate(email, password);

    const accessToken = TokenService.generateAccessToken(user);
    const refreshToken = TokenService.generateRefreshToken(user.id);

    res.json({
      accessToken,
      refreshToken,
      expiresIn: 3600
    });
  } catch (error) {
    const authError = handleAuthError(error);
    res.status(authError.statusCode).json({
      error: authError.code,
      message: authError.message
    });
  }
});
Enter fullscreen mode Exit fullscreen mode

Testing JWT Implementation

import jwt from 'jsonwebtoken';
import { TokenService } from './token.service';

describe('TokenService', () => {
  const mockUser = {
    id: '123',
    email: 'test@example.com',
    role: UserRole.USER
  };

  it('should generate valid access token', () => {
    const token = TokenService.generateAccessToken(mockUser);
    const decoded = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload;

    expect(decoded.sub).toBe(mockUser.id);
    expect(decoded.email).toBe(mockUser.email);
    expect(decoded.role).toBe(mockUser.role);
  });
});
Enter fullscreen mode Exit fullscreen mode

Related Resources

Conclusion

Node.js and TypeScript provide a robust foundation for implementing JWT authentication. By following TypeScript best practices and security considerations, you can build secure and maintainable authentication systems.

Remember to check our other security guides and authentication tools for more resources!

Common Pitfalls and Best Practices

When implementing JWT authentication, be aware of these common issues:

  1. Token Storage

    • Never store tokens in localStorage (XSS vulnerable)
    • Use httpOnly cookies for better security
    • Consider memory storage for SPAs
  2. Security Headers

    • Always use HTTPS
    • Implement CORS properly
    • Set secure headers (HSTS, CSP, etc.)
  3. Token Lifetime

    • Keep access tokens short-lived (15-60 minutes)
    • Use refresh tokens for longer sessions
    • Implement proper token rotation

Use 400+ completely free and online tools at Tooleroid.com!

Top comments (0)