Authentication in React (JWT)

Loading

Authentication is a key part of building web applications. It allows you to verify the identity of users, manage their sessions, and protect sensitive data. One of the most popular ways to implement authentication in modern web applications is by using JSON Web Tokens (JWT).

JWT is an open standard for securely transmitting information between parties as a JSON object. It’s widely used for authentication because of its compact size, flexibility, and ability to be passed through URL parameters, HTTP headers, or cookies.

In this guide, we’ll walk through how to implement JWT-based authentication in a React application.


1. How JWT Authentication Works

  • User Login: The user provides credentials (like username and password). The server verifies these credentials, and if they are valid, the server generates a JWT.
  • JWT Storage: The generated JWT is sent to the client (React app) and is stored (usually in local storage or cookies).
  • API Requests: For every subsequent request, the React app sends the JWT in the HTTP request (usually in the Authorization header).
  • Token Verification: The server verifies the JWT to ensure it’s valid, not expired, and signed by the correct secret key before allowing access to protected resources.

2. Setting Up JWT Authentication in React

Install Axios (for HTTP Requests)

We’ll use Axios to send HTTP requests to the server.

npm install axios

3. Creating the React App with JWT Authentication

Here’s a step-by-step implementation of JWT-based authentication in React.

1. Login Form Component

Create a simple login form where users can enter their credentials to log in.

import React, { useState } from 'react';
import axios from 'axios';

function Login({ setAuth }) {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const response = await axios.post('https://your-api-url.com/login', { email, password });
      // Save JWT to localStorage or cookies
      localStorage.setItem('token', response.data.token);
      setAuth(true); // Set authentication state to true
    } catch (error) {
      setError('Invalid credentials. Please try again.');
    }
  };

  return (
    <div>
      <h2>Login</h2>
      <form onSubmit={handleSubmit}>
        <div>
          <label>Email</label>
          <input 
            type="email" 
            value={email} 
            onChange={(e) => setEmail(e.target.value)} 
            required 
          />
        </div>
        <div>
          <label>Password</label>
          <input 
            type="password" 
            value={password} 
            onChange={(e) => setPassword(e.target.value)} 
            required 
          />
        </div>
        {error && <p style={{ color: 'red' }}>{error}</p>}
        <button type="submit">Login</button>
      </form>
    </div>
  );
}

export default Login;
  • Handle Form Submission: The login form captures user credentials and sends them to the server.
  • Save JWT: Upon successful authentication, the JWT is stored in localStorage.

2. App Component (Managing Authentication State)

In the App.js, we need to manage the authentication state. We’ll track whether the user is authenticated or not.

import React, { useState, useEffect } from 'react';
import Login from './Login';

function App() {
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  useEffect(() => {
    // Check if there's a valid token in localStorage
    const token = localStorage.getItem('token');
    if (token) {
      setIsAuthenticated(true);
    }
  }, []);

  return (
    <div>
      {isAuthenticated ? (
        <h1>Welcome, you are logged in!</h1>
      ) : (
        <Login setAuth={setIsAuthenticated} />
      )}
    </div>
  );
}

export default App;
  • Use useEffect: On initial render, we check if there’s a token stored in localStorage. If there is, we assume the user is authenticated.
  • Display Based on Auth: If the user is authenticated, we show a welcome message; otherwise, the login form is shown.

3. Making Authenticated API Requests

To make authenticated API requests, we’ll need to include the JWT in the Authorization header.

import axios from 'axios';

const api = axios.create({
  baseURL: 'https://your-api-url.com',
});

// Set up a request interceptor to include the token in the headers
api.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('token');
    if (token) {
      config.headers['Authorization'] = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

export default api;
  • Interceptors: We use Axios interceptors to attach the JWT token to every request that requires authentication. This ensures that the token is included in the request headers automatically.

4. Example: Protected Resource Request

To fetch data from a protected API endpoint, you can use the Axios instance we configured earlier.

import React, { useEffect, useState } from 'react';
import api from './api';

function ProtectedPage() {
  const [data, setData] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await api.get('/protected-route'); // Protected endpoint
        setData(response.data);
      } catch (error) {
        console.log('Error fetching protected data:', error);
      }
    };

    fetchData();
  }, []);

  if (!data) return <div>Loading...</div>;

  return (
    <div>
      <h1>Protected Data</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

export default ProtectedPage;
  • Authenticated Request: The api instance will automatically include the JWT token in the headers for the request to the protected route.

4. Handling Token Expiry

JWT tokens typically have an expiration time, and once expired, users need to re-authenticate. You can handle this in multiple ways:

  • Token Expiry Handling on the Client: If you detect an expired token in a response, you can either redirect the user to the login page or refresh the token (if you have a refresh token mechanism).
  • Logout: When a user logs out, you should remove the token from localStorage.

Example of handling token expiry:

api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response.status === 401) {
      // Token expired or invalid
      localStorage.removeItem('token');
      window.location.href = '/login'; // Redirect to login page
    }
    return Promise.reject(error);
  }
);

5. Logout Feature

To implement logout functionality, you can create a simple button that clears the stored JWT token and redirects the user to the login page.

function Logout() {
  const handleLogout = () => {
    localStorage.removeItem('token');
    window.location.href = '/login';
  };

  return <button onClick={handleLogout}>Logout</button>;
}

export default Logout;

6. Security Considerations

  • Store JWT Securely: JWT tokens should be stored securely. While storing them in localStorage is common, it’s vulnerable to cross-site scripting (XSS) attacks. Consider storing tokens in HTTP-only cookies to prevent access from JavaScript.
  • Token Expiration: Always handle token expiration gracefully by prompting users to log in again or refresh the token.
  • Secure Connections: Ensure that your React app communicates with the backend over HTTPS to prevent man-in-the-middle (MITM) attacks.

Leave a Reply

Your email address will not be published. Required fields are marked *