JWT Authentication in Spring Boot API

Implementing JWT Authentication in Spring Boot API

JWT (JSON Web Token) is a widely-used approach for securing APIs by utilizing token-based authentication, ensuring that only authenticated users can access your API endpoints. Unlike conventional session-based authentication, JWT is stateless, meaning it eliminates the need for server-side session storage, making it a great fit for scalable and distributed systems. In this guide, we'll walk you through setting up JWT authentication in a Spring Boot API, covering everything from generating tokens during user login to protecting your endpoints by validating these tokens, ultimately strengthening the security of your application's resources.

Prerequisites

  • JAVA 17
  • Maven

Project structure

├─ pom.xml
└─ src
   └─ main
      ├─ java
      │  └─ com
      │     └─ stackpuz
      │        └─ example
      │           ├─ App.java
      │           ├─ config
      │           │  ├─ JwtAuthenticationFilter.java
      │           │  └─ WebSecurityConfig.java
      │           └─ controller
      │              └─ AppController.java
      └─ resources
         ├─ application.properties
         └─ static
            ├─ index.html
            └─ login.html

Project files

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.stackpuz</groupId>
	<artifactId>example-jwt</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>example-jwt</name>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.0.10</version>
	</parent>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
	        <groupId>org.springframework.boot</groupId>
	        <artifactId>spring-boot-devtools</artifactId>
	    </dependency>
		<dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
		<dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.1</version>
        </dependency>
	</dependencies>
</project>

This pom.xml file defines the configuration and dependencies for the Maven project.

application.properties

jwt_secret = b0WciedNJvFCqFRbB2A1QhZoCDnutAOen5g1FEDO0HsLTwGINp04GXh2OXVpTqQL
spring.main.allow-circular-references = true

This Spring Boot configuration file manages the JWT secret key used for token signing and authentication.

App.java

package com.stackpuz.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class App {
	public static void main(String[] args) {
		SpringApplication.run(App.class, args);
	}
}

App.java is the main entry point for this Spring Boot application, this class launches the application without automatic database setup.

JwtAuthenticationFilter.java

package com.stackpuz.example.config;

import java.io.IOException;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import io.jsonwebtoken.Jwts;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    @Autowired
    private String jwtSecret;
    
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
        String header = req.getHeader("Authorization");
        if (header == null || !header.startsWith("Bearer ")) {
            res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }
        else {
            String token = header.substring(7);
            try {
                String username = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody().getSubject();
                UsernamePasswordAuthenticationToken authenToken = new UsernamePasswordAuthenticationToken(username, null);
                SecurityContextHolder.getContext().setAuthentication(authenToken);
            }
            catch (Exception e) {
                res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                return;
            }
        }
        chain.doFilter(req, res);
    }

    protected boolean shouldNotFilter(HttpServletRequest request) {
	    RequestMatcher matcher = new OrRequestMatcher(
	    	new AntPathRequestMatcher("/"),
            new AntPathRequestMatcher("/login"),
            new AntPathRequestMatcher("/index.html"),
	    	new AntPathRequestMatcher("/login.html")
	    );
	    return matcher.matches(request);
    }
}

JwtAuthenticationFilter.java is a security component that validates JWT tokens in HTTP request headers. It filters incoming requests by checking their Authorization headers, excluding public endpoints like login and index pages. The filter verifies JWT tokens using a secret key and establishes user authentication if the token is valid.

WebSecurityConfig.java

package com.stackpuz.example.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Value("${jwt_secret}")
    private String jwtSecret;

    @Bean
    public String jwtSecret() {
        return jwtSecret;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
        .csrf().disable()
        .authorizeHttpRequests()
        .requestMatchers("/", "/login", "/user", "/index.html", "/login.html").permitAll()
        .anyRequest().authenticated()
        .and().addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}

WebSecurityConfig.java is a configuration class that sets up Spring Security settings. It configures JWT-based authentication by integrates the JwtAuthenticationFilter before the default authentication process, disables CSRF protection, defines public endpoints ('/login', '/index.html', etc.), and requires authentication for all other requests. The class also manages the JWT secret key used for token verification.

AppController.java

package com.stackpuz.example.controller;

import java.security.Principal;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

@Controller
public class AppController {

    @Autowired
    private String jwtSecret;

    @GetMapping("/user")
    public ResponseEntity<?> getUser(Principal principal) {
        Map<String, String> user = new HashMap<String, String>();
        user.put("name", principal.getName());
        return ResponseEntity.ok(user);
    }

    @PostMapping("/login")
    public ResponseEntity<?> Login(@RequestBody Map<String, String> login) throws Exception {
        String name = login.get("name");
        if (name.equals("admin") && login.get("password").equals("1234")) {
            UsernamePasswordAuthenticationToken authenToken = new UsernamePasswordAuthenticationToken(name, null);
            SecurityContextHolder.getContext().setAuthentication(authenToken);
            String token = Jwts.builder()
                .setSubject(name)
                .setExpiration(new Date(System.currentTimeMillis() + (60 * 60 * 24 * 1000)))
                .signWith(SignatureAlgorithm.HS256, jwtSecret).compact();
            Map<String, Object> response = new HashMap<String, Object>();
            response.put("token", token);
            return ResponseEntity.ok(response);
        }
        return ResponseEntity.status(400).build();
    }
}

The AppController.java class is a Spring Boot controller that provides two main functionalities: retrieving the authenticated user's name through a GET request at /user, and handling user login via a POST request at /login. Upon successful login (with predefined credentials), it generates a JWT token, signs it with a secret, and returns it in the response. The controller uses Spring Security for authentication and JWT for token generation.

index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet">
</head>
<body>
    <div class="container">
        <div class="row">
            <div class="col mt-5">
                <div class="d-flex justify-content-center">
                    <div class="card p-0">
                        <div class="card-header">
                            <h3>Home</h3>
                        </div>
                        <div class="card-body text-center">
                            <i id="icon" class="fa fa-times-circle fa-5x mt-3 text-danger"></i>
                            <p id="message" class="mt-3">
                                You are currently not logged in.
                                <br/>
                                Redirecting to login page in seconds..
                            </p>
                            <div id="logout" class="col-12 d-none">
                                <button class="btn btn-primary w-100" onclick="logout()">Logout</button>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <script>
        function init() {
            fetch('/user', {
                headers: {
                    Authorization: 'Bearer ' + localStorage.getItem('token')
                }
            }).then(response => {
                if (response.ok) {
                    response.json().then(data => {
                        document.getElementById('icon').className = 'fa fa-check-circle fa-5x mt-3 text-success'
                        document.getElementById('message').innerText = 'You are currently logged in as: ' + data.name
                        document.getElementById('logout').classList.remove('d-none')
                    })
                }
                else {
                    if (response.status === 401) {
                        setTimeout(() => {
                            location = '/login.html'
                        }, 4000)
                    }
                    else {
                        alert(`Error: ${response.status} ${response.statusText}`)
                    }
                }
            })
        }

        function logout() {
            localStorage.removeItem('token')
            location.reload()
        }

        init()
    </script>
</body>

The index.html is a simple web page that provides a user interface for displaying the login status of a user. It uses Bootstrap for styling and Font Awesome for icons. On page load, it checks the user's authentication status by sending a request to the server with a JWT token stored in localStorage. If the user is logged in, it shows a success message with the user's name and a logout button. If not logged in, it shows a message indicating the user is not logged in and redirects them to the login page after a few seconds.

login.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet">
</head>
<body>
    <div class="container">
        <div class="row">
            <div class="col mt-5">
                <div class="d-flex justify-content-center">
                    <div class="card p-0" style="width: 300px;">
                        <div class="card-header">
                            <h3>Login</h3>
                        </div>
                        <div class="card-body">
                            <i class="fa fa-user-circle fa-5x d-block text-center text-secondary"></i>
                            <form onsubmit="return login()">
                                <div class="row">
                                    <div class="mb-3 col-12">
                                        <label class="form-label" for="user_account_name">User Name</label>
                                        <input id="name" class="form-control form-control" value="admin" required/>
                                    </div>
                                    <div class="mb-3 col-12">
                                        <label class="form-label" for="user_account_password">Password</label>
                                        <input id="password" class="form-control form-control" type="password" value="1234" required/>
                                    </div>
                                    <div class="col-12">
                                        <button class="btn btn-primary w-100">Login</button>
                                    </div>
                                </div>
                            </form>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <script>
        function login() {
            fetch('/login', {
                method: 'POST',
                body: JSON.stringify({
                    name: document.getElementById('name').value,
                    password: document.getElementById('password').value
                }),
                headers: {
                    'Content-Type': 'application/json'
                }
            }).then(response => {
                if (response.ok) {
                    response.json().then(data => {
                        localStorage.setItem('token', data.token)
                        location = '/'
                    })
                }
                else {
                    alert(`Error: ${response.status} ${response.statusText}`)
                }
            })
            return false
        }
    </script>
</body>

The login.html page provides a simple login form where users can input their username and password. It uses Bootstrap for styling and Font Awesome for icons. When the user submits the form, a JavaScript function login() sends a POST request to the /login endpoint with the entered credentials. If the login is successful, the server returns a JWT token, which is stored in localStorage. The page then redirects the user to the home page (/). If the login fails, an error message is displayed.

Run project

mvn spring-boot:run

Open the web browser and goto http://localhost:8080
You will find this test page.

home page

Testing

After a few seconds, you will be redirected to the login page.

login page

Press the login button, and you will be logged in to the home page, which will display the logged-in user's name.

logged in

Try refreshing the browser, and you will see that you're still logged in. Then, press the logout button, the JWT token will be removed, and you will be redirected to the login page again.

Conclusion

In conclusion, implementing JWT authentication in a Spring Boot API provides a robust and scalable solution for securing applications. By leveraging Spring Security and the io.jsonwebtoken library, we can easily authenticate users, issue tokens, and manage secure communication between the client and server. This approach ensures that user sessions are maintained securely using stateless authentication, reducing the risk of server-side vulnerabilities. The flexibility of JWT also allows for seamless integration with front-end applications, enabling smooth user experiences. With proper configuration and security practices, JWT authentication becomes a reliable method for building secure and efficient APIs in Spring Boot.

Source code: https://github.com/stackpuz/Example-JWT-Spring-Boot-3

Create a CRUD Web App in Minutes: https://stackpuz.com

Related post