Lesson 07-Axios Security and Authentication

Handling JWT (JSON Web Tokens) Authentication

In modern web applications, security is paramount, especially for user authentication and authorization. JSON Web Tokens (JWT) are a popular authentication mechanism where the server issues a token to the client, which the client stores locally and includes in subsequent requests as proof of identity.

JWT Authentication Flow

  • Login Request: The user sends a username and password to the server.
  • Server Validation: The server verifies the credentials and, if successful, generates a JWT and returns it to the client.
  • Token Storage: The client (typically a browser) stores the JWT, often in localStorage or sessionStorage.
  • Attaching to Requests: For authenticated requests, the client includes the JWT in the request header, typically in the Authorization header as Bearer <token>.

Using Axios for JWT

Axios can automatically attach a JWT to every request using a request interceptor, ensuring all requests carry valid authentication information.

Example Code:

import axios from 'axios';

// Create Axios instance
const apiClient = axios.create({
  baseURL: 'https://api.example.com',
});

// Request interceptor
apiClient.interceptors.request.use(
  config => {
    // Retrieve JWT from localStorage
    const token = localStorage.getItem('jwtToken');
    // Attach JWT to headers if it exists
    if (token) {
      config.headers['Authorization'] = `Bearer ${token}`;
    }
    return config;
  },
  error => {
    return Promise.reject(error);
  }
);

// Response interceptor
apiClient.interceptors.response.use(
  response => {
    return response;
  },
  error => {
    // Handle 401 status for expired or invalid JWT
    if (error.response && error.response.status === 401) {
      // Clear stored JWT
      localStorage.removeItem('jwtToken');
      // Redirect to login page
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

export default apiClient;

Handling JWT Expiration and Refresh

JWTs typically have a limited lifespan. When a JWT expires, the client needs to refresh it, often using a refresh token to obtain a new JWT.

JWT Refresh Example:

// Function to refresh JWT
const refreshJWT = async () => {
  try {
    const response = await apiClient.post('/refresh-token');
    const newToken = response.data.token;
    localStorage.setItem('jwtToken', newToken);
    return newToken;
  } catch (error) {
    // Handle refresh failure
    localStorage.removeItem('jwtToken');
    window.location.href = '/login';
    throw error;
  }
};

In practice, you may need to check if a JWT is nearing expiration in the request interceptor and call refreshJWT if necessary.

Step 1: Get JWT Expiration Time

JWTs often include an exp field indicating the expiration time (UNIX timestamp). Parse the JWT to retrieve this timestamp.

function getJwtExpirationDate(token) {
  const decoded = jwt_decode(token);
  if (!decoded.exp) return null;
  return new Date(0).setUTCSeconds(decoded.exp);
}

This uses the jwt-decode library to parse JWTs. Install it with npm install jwt-decode.

Step 2: Calculate JWT Remaining Time

Compare the current time with the JWT’s expiration time to determine the remaining validity.

function isTokenExpired(token) {
  const now = new Date().getTime();
  const expirationDate = getJwtExpirationDate(token);
  return expirationDate < now;
}

function isTokenExpiringSoon(token, bufferTime = 60 * 60 * 1000) { // Buffer time set to 1 hour
  const now = new Date().getTime();
  const expirationDate = getJwtExpirationDate(token);
  return expirationDate - now < bufferTime;
}

Step 3: Refresh JWT in Request Interceptor

Check if the JWT is nearing expiration in the request interceptor and refresh it if needed.

import axios from 'axios';
import jwt_decode from 'jwt-decode';

const apiClient = axios.create({
  baseURL: 'https://api.example.com',
});

apiClient.interceptors.request.use(
  async config => {
    const token = localStorage.getItem('jwtToken');
    if (token && isTokenExpiringSoon(token)) {
      try {
        const newToken = await refreshJWT(token);
        localStorage.setItem('jwtToken', newToken);
        config.headers['Authorization'] = `Bearer ${newToken}`;
      } catch (error) {
        // Handle refresh failure
        localStorage.removeItem('jwtToken');
        window.location.href = '/login';
      }
    }
    return config;
  },
  error => {
    return Promise.reject(error);
  }
);

async function refreshJWT(token) {
  // Send refresh request
  const response = await apiClient.post('/refresh-token', { refreshToken: token });
  return response.data.token;
}

export default apiClient;

Notes

  • Ensure refreshJWT handles failures like network issues or server errors.
  • Adjust the buffer time in isTokenExpiringSoon to suit your application’s needs, ensuring JWTs are refreshed before expiration.
  • In production, consider safer JWT storage methods, like HttpOnly cookies, to prevent cross-site scripting (XSS) attacks.

Using Axios with OAuth2 Authentication

OAuth2 is an open standard protocol for authorizing applications to access protected resources without exposing user credentials. It involves the following roles:

  • Resource Owner (user)
  • Client (application)
  • Resource Server (service providing protected resources)
  • Authorization Server (service issuing access tokens)

OAuth2 Flow Overview

  1. Authorization Request: The client directs the user to the authorization server’s endpoint.
  2. User Authorization: The user logs in and authorizes the client to access their resources.
  3. Authorization Code: The authorization server returns an authorization code to the client.
  4. Token Request: The client uses the code to request an access token from the authorization server.
  5. Token Response: The server returns an access token and possibly a refresh token.
  6. Resource Access: The client uses the access token to access protected resources on the resource server.

Using Axios for OAuth2 Authentication

Using Axios for OAuth2 authentication typically involves these steps:

  1. Obtain Access Token: Send a POST request to the authorization server’s token endpoint with the client ID, client secret, authorization code, etc.
const axios = require('axios');

const clientID = 'your-client-id';
const clientSecret = 'your-client-secret';
const authorizationCode = 'the-authorization-code-received';
const redirectURI = 'your-redirect-uri';
const tokenEndpoint = 'https://auth.example.com/oauth/token';

axios.post(tokenEndpoint, {
  grant_type: 'authorization_code',
  code: authorizationCode,
  redirect_uri: redirectURI,
  client_id: clientID,
  client_secret: clientSecret
})
.then(response => {
  console.log(response.data.access_token);
})
.catch(error => {
  console.error(error);
});
  1. Store Access Token: Store the access token in the client (e.g., browser’s localStorage or sessionStorage).
  2. Attach Access Token: Include the access token in the Authorization header for requests to protected resources, using a request interceptor or direct configuration.
const apiClient = axios.create({
  baseURL: 'https://api.example.com'
});

apiClient.interceptors.request.use(config => {
  const accessToken = localStorage.getItem('access_token');
  if (accessToken) {
    config.headers.Authorization = `Bearer ${accessToken}`;
  }
  return config;
});

export default apiClient;
  1. Handle Token Expiration: When the access token expires, use a refresh token (if available) to request a new access token.
async function refreshAccessToken(refreshToken) {
  const tokenEndpoint = 'https://auth.example.com/oauth/token';
  const response = await axios.post(tokenEndpoint, {
    grant_type: 'refresh_token',
    refresh_token: refreshToken,
    client_id: clientID,
    client_secret: clientSecret
  });
  return response.data.access_token;
}

Setting Secure HTTP Headers (e.g., CORS)

HTTP headers carry metadata about requests and responses. Setting appropriate headers is critical for enhancing web application security. Below are common secure HTTP headers and how to configure them:

Content-Security-Policy (CSP)

CSP restricts resources loaded on a webpage to prevent cross-site scripting (XSS) attacks by specifying trusted sources.

Setting CSP:

add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://trusted-cdn.example.com; img-src 'self' data: https://trusted-image-host.example.com";

X-Frame-Options

This header prevents clickjacking by controlling whether a page can be displayed in <frame>, <iframe>, <embed>, or <object>.

Setting X-Frame-Options:

add_header X-Frame-Options DENY;

X-XSS-Protection

This header enables or disables the browser’s built-in XSS protection mechanism.

Setting X-XSS-Protection:

add_header X-XSS-Protection "1; mode=block";

X-Content-Type-Options

This header prevents MIME-type sniffing, ensuring browsers respect the response’s declared MIME type.

Setting X-Content-Type-Options:

add_header X-Content-Type-Options nosniff;

Strict-Transport-Security (HSTS)

HSTS enforces HTTPS connections, mitigating man-in-the-middle (MITM) attacks.

Setting HSTS:

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";

Cross-Origin Resource Sharing (CORS)

CORS allows a webpage from one domain to request resources from another, using additional headers to inform browsers about permitted cross-origin requests.

Setting CORS:

add_header Access-Control-Allow-Origin "*"; # Allow all origins
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS"; # Allowed methods
add_header Access-Control-Allow-Headers "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization";
add_header Access-Control-Max-Age 1728000; # Preflight request cache duration
add_header Access-Control-Allow-Credentials true; # Allow cookies

Tools for Setting Secure Headers

In Node.js, the helmet library simplifies setting these HTTP headers.

Using Helmet:

const express = require('express');
const helmet = require('helmet');

const app = express();

app.use(helmet());

app.listen(3000);
Share your love