Module 2 - JSON Web Tokens (JWT)

JSON Web Tokens (JWT)

JSON Web Tokens (JWT) are an open, industry-standard method for representing claims securely between two parties. They are commonly used in modern web applications for stateless authentication and information exchange.

Learning Objectives

JWT Fundamentals

Understand the structure and components of JWTs, including headers, payloads, and signatures.

Token Implementation

Learn how to generate, verify, and use JWTs for secure authentication in web applications.

Security Best Practices

Master JWT security considerations, including token expiration, refresh tokens, and proper storage.

Module Content

JWT Basics

  • What are JWTs?
  • JWT Structure
  • Token Components

Implementation

  • Token Generation
  • Token Verification
  • Token Storage

Security

  • Token Expiration
  • Refresh Tokens
  • Security Considerations

JWT Structure and Components

JWT Components

JSON Web Tokens (JWT) are a way to transmit information between parties in the form of a JSON object. The JSON information is most commonly used for authentication and information exchange.

A JWT is a string that has three parts separated by a period (.). Those are:

  • The header
  • The payload
  • The signature

Header

The header contains the algorithm with the token type. Typically the header for a JWT looks like this:

{
  "alg": "HS256",
  "typ": "JWT"
}

The alg key specifies which algorithm was used to create the token, in this case, the algorithm is HMACSHA-256, and the typ property classifies this token as being of the type JWT.

JWT Payload

The payload includes claims (fancy name for things like permissions for the user) information or any other data we'd like to store in the token, which is most likely a user id. There are specific claims defined in the JWT standard, and you can also add custom properties to this object.

An example payload:

{
  "sub": "1234567890", // standard - subject, normally the user id
  "name": "John Doe", // custom property
  "iat": 1516239022 // standard - The Date the token was issued, expressed in seconds since epoch.
}

Standard Claims

  • sub (subject): Entity to whom the token belongs, usually a user ID
  • iat (issued at): Time at which the token was issued
  • exp (expiration time): After this time, the token is invalid
  • nbf (not before): Token can't be used before this time
  • iss (issuer): Identifies who issued the token

JWT Security Implementation

To create a signature, we must create a string by base64 encoding the header and payload together and then signing it with a secret.

The signature is created using the following pseudocode:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)

Combining all three parts, you will get a JWT that looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

You can visit jwt.io and use the debugger to see an excellent representation of a JWT.

JWT Implementation

Producing and Sending JWTs

When implementing JWT-based authentication, we need to generate tokens during login and verify them on subsequent requests.

Code Example - Creating JWTs with Node.js:

const jwt = require('jsonwebtoken');

// Secret key for signing tokens (store in env variables in production)
const JWT_SECRET = 'your_jwt_secret';

// Function to generate a token
function generateToken(user) {
  const payload = {
    subject: user.id,        // standard claim for user id
    username: user.username, // custom data
    role: user.role,         // custom data for authorization
    iat: Date.now() / 1000   // issued at time in seconds
  };
  
  const options = {
    expiresIn: '1h' // token expires in 1 hour
  };
  
  return jwt.sign(payload, JWT_SECRET, options);
}

Login with JWT Authentication

Once we have the token generation function, we can implement a login endpoint:

Code Example - Login Route with JWT:

// Login endpoint
app.post('/api/login', async (req, res) => {
  const { username, password } = req.body;
  
  try {
    // Verify credentials (pseudo code)
    const user = await getUserByUsername(username);
    
    // Check if user exists and password matches
    if (!user || !await bcrypt.compare(password, user.password)) {
      return res.status(401).json({
        message: 'Invalid credentials'
      });
    }
    
    // Generate token
    const token = generateToken(user);
    
    // Send the token in the response
    res.status(200).json({
      message: 'Login successful',
      token
    });
  } catch (error) {
    res.status(500).json({
      message: 'Server error during login'
    });
  }
});

JWT Authentication Middleware

To protect routes and require authentication, we need middleware to verify the JWT:

Code Example - JWT Authentication Middleware:

// JWT authentication middleware
function authenticateToken(req, res, next) {
  // Get token from Authorization header
  const authHeader = req.headers.authorization;
  const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
  
  if (!token) {
    return res.status(401).json({ 
      message: 'Access denied. No token provided.' 
    });
  }
  
  try {
    // Verify the token
    const decoded = jwt.verify(token, JWT_SECRET);
    
    // Add the decoded token to the request object
    req.user = decoded;
    
    // Continue to the next middleware/route handler
    next();
  } catch (error) {
    return res.status(403).json({ 
      message: 'Invalid token.' 
    });
  }
}

// Example of protected route using JWT
app.get('/api/protected', authenticateToken, (req, res) => {
  res.json({ 
    message: 'This is protected data',
    user: req.user
  });
});

JWT Security Best Practices

JWT Security Considerations

When using JWTs, we need to consider several security aspects:

1. Token Expiration

Always set a reasonable expiration time on tokens to limit the damage if a token is compromised. Use the expiresIn option when signing tokens.

2. Keep Secrets Secret

Store JWT secrets in environment variables, never hardcode them into your application.

3. HTTPS Only

Always use HTTPS in production to protect tokens in transit. JWTs sent over HTTP are vulnerable to interception.

4. Minimize Payload Size

Keep the payload small. Don't include sensitive data in it as the payload can be easily decoded (though not tampered with due to signature verification).

Refresh Tokens

For improved security, use a two-token system: short-lived access tokens and longer-lived refresh tokens.

Code Example - Refresh Token Implementation:

// Generate both access and refresh tokens
function generateTokens(user) {
  const accessToken = jwt.sign(
    { subject: user.id, username: user.username },
    JWT_ACCESS_SECRET,
    { expiresIn: '15m' } // short-lived
  );
  
  const refreshToken = jwt.sign(
    { subject: user.id },
    JWT_REFRESH_SECRET,
    { expiresIn: '7d' } // longer-lived
  );
  
  return { accessToken, refreshToken };
}

// Login route providing both tokens
app.post('/api/login', async (req, res) => {
  // ... verify credentials
  
  const tokens = generateTokens(user);
  
  // Store refresh token in DB (important for token revocation)
  await storeRefreshToken(user.id, tokens.refreshToken);
  
  res.json(tokens);
});

// Refresh token endpoint
app.post('/api/refresh-token', async (req, res) => {
  const { refreshToken } = req.body;
  
  if (!refreshToken) {
    return res.status(401).json({ message: 'Refresh token required' });
  }
  
  try {
    // Verify the refresh token
    const decoded = jwt.verify(refreshToken, JWT_REFRESH_SECRET);
    
    // Check if token is in our database and not revoked
    const storedToken = await getRefreshToken(decoded.subject, refreshToken);
    
    if (!storedToken) {
      return res.status(403).json({ message: 'Invalid refresh token' });
    }
    
    // Get user and generate new access token
    const user = await getUserById(decoded.subject);
    const accessToken = jwt.sign(
      { subject: user.id, username: user.username },
      JWT_ACCESS_SECRET,
      { expiresIn: '15m' }
    );
    
    res.json({ accessToken });
  } catch (error) {
    res.status(403).json({ message: 'Invalid refresh token' });
  }
});

Client-Side Handling

Proper client-side handling of JWTs is crucial for security:

1. Storage Location

There are several options for storing JWTs in the browser:

  • localStorage: Convenient but vulnerable to XSS attacks
  • httpOnly Cookies: More secure against XSS but requires proper CSRF protection
  • In-memory storage: Safest option but lost on page refresh

Code Example - Client-side JWT handling:

// Example React hook for JWT authentication
function useAuth() {
  const [token, setToken] = useState(null);
  const [user, setUser] = useState(null);
  
  // Login function
  const login = async (username, password) => {
    try {
      const response = await fetch('/api/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();
      
      // Store token in memory (not localStorage for security)
      setToken(data.token);
      
      // Decode user info from token
      const decoded = parseJwt(data.token);
      setUser(decoded);
      
      return true;
    } catch (error) {
      console.error('Login error:', error);
      return false;
    }
  };
  
  // Function to add token to requests
  const authFetch = async (url, options = {}) => {
    if (!token) throw new Error('No authentication token');
    
    const headers = {
      ...options.headers,
      'Authorization': `Bearer ${token}`
    };
    
    return fetch(url, { ...options, headers });
  };
  
  return { user, login, authFetch };
}

Guided Project

JWT Implementation

Build a secure authentication system using JWT with Node.js and Express

View Project

Resources

Tools & Libraries

  • jsonwebtoken - For JWT handling
  • express-jwt - Express middleware for JWT
  • jwt-decode - For client-side JWT decoding