logo
Basic Utils
Home

Implementing JWT Authentication with Spring Boot 3

Introduction

JSON Web Tokens (JWT) have become a popular choice for securing APIs. In this tutorial, we will implement JWT authentication using Spring Boot 3.0, following best practices and ensuring a robust security setup. We will cover the entire process from project setup to implementing security features using JWT.

What is JWT?

JWT is an open standard (RFC 7519) for securely transmitting information between parties as a JSON object. It is compact, URL-safe, and can be verified and trusted because it is digitally signed. JWTs are commonly used for authentication and information exchange.

A JWT typically consists of three parts:

  • Header: Contains metadata about the token, such as the type (JWT) and signing algorithm (e.g., HMAC SHA256).
  • Payload: Contains the claims or information about the user and other data.
  • Signature: Used to verify the sender of the JWT and ensure that the message wasn't changed.

JWT Authentication Flow

The JWT authentication process generally follows these steps:

  1. The user sends their credentials (username and password) to the server.
  2. If the credentials are valid, the server generates a JWT and sends it back to the user.
  3. The user stores the JWT (usually in local storage or a cookie).
  4. For subsequent requests, the user sends the JWT in the Authorization header.
  5. The server verifies the JWT and grants access to protected resources.

Setting Up the Project

We'll create a new Spring Boot project using Spring Initializr with the following configurations:

  • Project: Maven Project
  • Language: Java
  • Spring Boot: 3.0.0
  • Dependencies: Spring Web, Spring Security, Spring Data JPA, MySQL Driver, Spring Boot DevTools

Dependencies

Here are the Maven dependencies we will need:

  • spring-boot-starter-web
  • spring-boot-starter-security
  • spring-boot-starter-data-jpa
  • mysql-connector-java
  • spring-boot-starter-devtools
  • jjwt
    (for JWT handling)

Creating the User Model

First, we create a User model to represent our users. This model will contain fields like username, password, and roles.


package com.basicutils.jwtauthexample.model;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;
    private String password;
    private String role;

    // Getters and Setters
}

Implementing BCrypt

To securely store user passwords, we will use BCrypt hashing. BCrypt is a password hashing function that incorporates a salt to protect against rainbow table attacks.

We will use the BCryptPasswordEncoder provided by Spring Security to encode passwords before storing them in the database.


package com.basicutils.jwtauthexample.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class PasswordConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

Creating the JWT Util Class

This class will handle the creation and validation of JWT tokens.


package com.basicutils.jwtauthexample.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;

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

@Component
public class JwtUtil {

    private final String SECRET_KEY = "your_secret_key";

    public String generateToken(String username) {
        Map

User Details Service

We need a service to load user-specific data during authentication. We'll implement UserDetailsService to fetch user details from the database.


package com.basicutils.jwtauthexample.service;

import com.basicutils.jwtauthexample.model.User;
import com.basicutils.jwtauthexample.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username);
        return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), new ArrayList<>());
    }
}

Creating the JWT Request Filter

The JWT Request Filter will intercept requests and validate the JWT token before allowing access to protected routes.


package com.basicutils.jwtauthexample.filter;

import com.basicutils.jwtauthexample.service.UserDetailsServiceImpl;
import com.basicutils.jwtauthexample.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        final String authorizationHeader = request.getHeader("Authorization");

        String username = null;
        String jwt = null;

        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            jwt = authorizationHeader.substring(7);
            username = jwtUtil.extractUsername(jwt);
        }

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
            if (jwtUtil.validateToken(jwt, userDetails.getUsername())) {
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }
        chain.doFilter(request, response);
    }
}

Security Configuration

Now, we will set up the security configuration using Spring Security.


package com.basicutils.jwtauthexample.config;

import com.basicutils.jwtauthexample.filter.JwtRequestFilter;
import com.basicutils.jwtauthexample.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Autowired
    private JwtRequestFilter jwtRequestFilter;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/api/auth/**").permitAll()
                        .anyRequest().authenticated()
                )
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public AuthenticationManager authManager(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder authenticationManagerBuilder =
                http.getSharedObject(AuthenticationManagerBuilder.class);
        authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
        return authenticationManagerBuilder.build();
    }
}

Best Practices for JWT Authentication

When implementing JWT authentication, consider the following best practices:

  • Use HTTPS: Always use HTTPS to encrypt data in transit, including JWTs.
  • Set Short Expiration Times: Keep JWT expiration times short to limit the impact of token theft.
  • Use Refresh Tokens: Implement a refresh token mechanism for better security and user experience.
  • Store Tokens Securely: Store JWTs in a secure place, such as HttpOnly cookies, to prevent XSS attacks.
  • Rotate Secrets: Regularly rotate your JWT signing secrets to minimize risks.
  • Validate Tokens: Always validate JWTs on the server-side to ensure they are genuine and not expired.

Conclusion

In this tutorial, we covered how to implement JWT authentication with Spring Boot 3.0. We discussed JWT basics, BCrypt password hashing, and the entire flow of JWT authentication. By following best practices, you can enhance the security of your applications significantly.

References

logo
Basic Utils

simplify and inspire technology

©2024, basicutils.com