JWT Authentication in React: Login, Logout, Protected Routes

In this module, we will cover how to implement JWT (JSON Web Token) authentication in a React application. We will go through the process of setting up a login system, managing authentication states, handling user logouts, and securing protected routes with JWTs. This will involve integrating the front end (React) with a back-end service that issues the JWTs and validates them on the server side.

JWTs are widely used for securing web applications due to their stateless nature, which means the server doesn’t have to maintain session data between requests.


Table of Contents

  1. Understanding JWT Authentication
  2. Setting Up a Simple Backend with JWT Authentication
  3. Login Flow: Sending Credentials and Storing JWT
  4. Logout Flow: Clearing JWT and Redirecting
  5. Protected Routes: Restricting Access to Authenticated Users
  6. Best Practices for Handling JWT in React
  7. Conclusion

1. Understanding JWT Authentication

JWTs are a compact, URL-safe way to represent claims between two parties, often used for user authentication and information exchange. The token is typically issued by the server upon successful authentication and can be sent by the client on every subsequent request for authentication purposes.

A JWT consists of three parts:

  1. Header: Contains the algorithm used to sign the token (usually HS256).
  2. Payload: Contains the claims, i.e., the data (such as user ID, username, roles, etc.).
  3. Signature: A hash created from the header and payload, using a secret key to prevent tampering.

JWTs are stored on the client side (usually in localStorage or sessionStorage) and included in HTTP request headers (Authorization: Bearer <token>) when making requests to secure endpoints.


2. Setting Up a Simple Backend with JWT Authentication

While this module focuses on the React front end, it’s important to understand how JWT authentication works with a backend.

Backend API Flow:

  1. User logs in by sending their credentials (username and password) to the backend.
  2. Backend validates credentials and, if valid, generates a JWT.
  3. Backend sends the JWT back to the frontend, which stores it (usually in localStorage or sessionStorage).
  4. On subsequent requests, the frontend sends the JWT in the Authorization header to access protected routes.
  5. The backend validates the JWT and grants or denies access based on its validity.

Here’s an example of how a backend might issue a JWT using Node.js with Express:

javascriptCopyEditconst express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const SECRET_KEY = 'your-secret-key';

// Middleware to parse JSON bodies
app.use(express.json());

// Mock users
const users = [
  { username: 'user1', password: 'password123' },
  { username: 'user2', password: 'password456' }
];

// POST /login endpoint to authenticate users
app.post('/login', (req, res) => {
  const { username, password } = req.body;

  // Find the user
  const user = users.find(u => u.username === username && u.password === password);
  
  if (!user) {
    return res.status(401).json({ message: 'Invalid credentials' });
  }

  // Generate JWT token
  const token = jwt.sign({ username: user.username }, SECRET_KEY, { expiresIn: '1h' });
  
  res.json({ token });
});

app.listen(5000, () => console.log('Server running on http://localhost:5000'));

3. Login Flow: Sending Credentials and Storing JWT

In the front end, the user will submit their credentials (username and password), and we will send a POST request to the backend for authentication. If authentication is successful, the backend will return a JWT, which we will store in localStorage or sessionStorage.

Example: React Login Form

javascriptCopyEditimport React, { useState } from 'react';
import { useHistory } from 'react-router-dom';

function LoginForm() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState(null);
  const history = useHistory();

  const handleSubmit = async (e) => {
    e.preventDefault();

    const response = await fetch('http://localhost:5000/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username, password })
    });

    const data = await response.json();

    if (response.ok) {
      // Store the JWT token in localStorage
      localStorage.setItem('token', data.token);
      history.push('/dashboard');
    } else {
      setError(data.message);
    }
  };

  return (
    <div>
      <h2>Login</h2>
      {error && <div style={{ color: 'red' }}>{error}</div>}
      <form onSubmit={handleSubmit}>
        <input 
          type="text" 
          placeholder="Username" 
          value={username} 
          onChange={(e) => setUsername(e.target.value)} 
        />
        <input 
          type="password" 
          placeholder="Password" 
          value={password} 
          onChange={(e) => setPassword(e.target.value)} 
        />
        <button type="submit">Login</button>
      </form>
    </div>
  );
}

export default LoginForm;

Storing JWT:

When the JWT is received, store it in localStorage or sessionStorage:

javascriptCopyEditlocalStorage.setItem('token', data.token);

Why localStorage vs sessionStorage?

  • localStorage: Data persists across sessions until manually cleared.
  • sessionStorage: Data is cleared when the page session ends (e.g., when the tab is closed).

4. Logout Flow: Clearing JWT and Redirecting

When a user logs out, you should clear the JWT from the client-side storage and redirect them to a public page (like the login page). This ensures the user cannot access protected routes after logging out.

Example: Logout Functionality

javascriptCopyEditimport React from 'react';
import { useHistory } from 'react-router-dom';

function Logout() {
  const history = useHistory();

  const handleLogout = () => {
    // Clear the JWT token from localStorage
    localStorage.removeItem('token');
    history.push('/login');
  };

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

export default Logout;

5. Protected Routes: Restricting Access to Authenticated Users

To restrict access to certain routes based on authentication, we can create a ProtectedRoute component. This component checks if a valid JWT is available in localStorage. If the JWT is missing or invalid, the user is redirected to the login page.

Example: Protected Route Component

javascriptCopyEditimport React from 'react';
import { Route, Redirect } from 'react-router-dom';

function ProtectedRoute({ component: Component, ...rest }) {
  const token = localStorage.getItem('token');

  return (
    <Route
      {...rest}
      render={(props) =>
        token ? (
          <Component {...props} />
        ) : (
          <Redirect to="/login" />
        )
      }
    />
  );
}

export default ProtectedRoute;

Using the Protected Route

You can now use this ProtectedRoute component to protect any route that requires authentication.

javascriptCopyEditimport { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import ProtectedRoute from './ProtectedRoute';
import Dashboard from './Dashboard';
import LoginForm from './LoginForm';

function App() {
  return (
    <Router>
      <Switch>
        <Route path="/login" component={LoginForm} />
        <ProtectedRoute path="/dashboard" component={Dashboard} />
      </Switch>
    </Router>
  );
}

export default App;

6. Best Practices for Handling JWT in React

Here are some best practices for securely handling JWTs in your React applications:

  • Never store sensitive data (like JWT) in the state: Storing JWT in React state can expose it to vulnerabilities like XSS (Cross-Site Scripting) attacks. It’s safer to use localStorage or sessionStorage for storing JWTs.
  • Secure JWT storage: Consider using HttpOnly cookies for more security. They are not accessible via JavaScript, protecting them from XSS attacks.
  • Token expiration: Always handle token expiration by checking the exp claim inside the JWT. If expired, prompt the user to log in again.
  • Refresh tokens: Consider implementing refresh tokens to extend the user session without requiring them to log in again.
  • Backend validation: Always verify the JWT on the server side before granting access to protected resources.

7. Conclusion

In this module, we learned how to implement JWT authentication in a React application. We covered the steps for setting up login, logout, and protected routes, ensuring that only authenticated users can access secure resources.

With JWT authentication, React developers can build secure, stateless applications where the client holds the user’s authentication token, reducing the server’s need to maintain session data. By following best practices and securing JWT storage and usage, we can create highly secure and user-friendly web applications.