Spring Boot Security JWT Authentication and Authorization Tutorial

In this tutorial, we'll build token-based authentication and role-based authorization using Spring Boot 3, Spring Security, JWT, and a MySQL database. We'll start by creating a Login REST API to authenticate users, generate a JWT, and return it in the response. This JWT will then be used to secure subsequent API requests. 

Spring Boot: A rapid application development framework that simplifies the process of developing Spring applications. It provides a range of starter dependencies, auto-configuration, and runtime features, allowing developers to quickly set up and create stand-alone, production-grade Spring applications with minimal configuration. 

Spring Security: A powerful and highly customizable authentication and access-control framework for Spring applications. It provides comprehensive security services for Java EE-based enterprise software applications, including protection against common exploits and the ability to integrate various authentication mechanisms (like LDAP, JWT, and OAuth2). 

JWT (JSON Web Token): An open standard (RFC 7519) for securely transmitting information between parties as a JSON object. JWTs are used primarily for authentication and information exchange, and they can be signed using a secret (with HMAC algorithm) or a public/private key pair using RSA. JWTs are compact, URL-safe, and can be used in web applications to manage user sessions and securely transfer identity information. 

MySQL: A popular open-source relational database management system (RDBMS) used for storing, retrieving, and managing data. It uses Structured Query Language (SQL) for database access and is known for its reliability, scalability, and ease of use. MySQL is frequently used as the database component in web applications, especially those built with Spring Boot. 

Together, these technologies provide a robust stack for building secure, scalable, and efficient web applications, with Spring Boot facilitating rapid development, Spring Security adding security layers, JWT handling secure authentication, and MySQL managing data storage.

Tools and Technologies used

  • Spring Boot 3.2.1
  • Spring Security
  • Spring Data JPA (Hibernate 6)
  • MySQL Database
  • Postman
  • Maven
  • IntelliJ IDEA
  • JWT Java library 0.12.3

Development Steps

1. Add Maven Dependencies
2. Configure MySQL Database
3. Create JPA Entities - User and Role (Many-to-Many Mapping)
4. Create Spring Data JPA Repositories
5. JWT Implementation Classes
6. Spring Security Implementation
7. Creating DTO classes - LoginDto and JwtAuthResponse
8. Creating a Service Layer
9. Controller Layer - Login REST API return JWT Token
10. Insert SQL Scripts
11. Testing using Postman
12. Implement Role-Based Authorization

Let's begin with adding Maven dependencies to your Spring Boot project.

1. Add Maven Dependencies

Add below Maven dependencies to your Spring Boot project:
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>com.mysql</groupId>
			<artifactId>mysql-connector-j</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>

		<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-impl</artifactId>
			<version>0.12.3</version>
			<scope>runtime</scope>
		</dependency>

		<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-api</artifactId>
			<version>0.12.3</version>
		</dependency>

		<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-jackson -->
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-jackson</artifactId>
			<version>0.12.3</version>
			<scope>runtime</scope>
		</dependency>

2. Configure MySQL Database

Let's first create a database in MySQL server using the below command:

create database login_system

Since we’re using MySQL as our database, we need to configure the database URL, username, and password so that Spring can establish a connection with the database on startup. Open the src/main/resources/application.properties file and add the following properties to it:

spring.datasource.url=jdbc:mysql://localhost:3306/login_system
spring.datasource.username=root
spring.datasource.password=Mysql@123

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
spring.jpa.hibernate.ddl-auto=update

app.jwt-secret=daf66e01593f61a15b857cf433aae03a005812b31234e149036bcc8dee755dbb
app.jwt-expiration-milliseconds=604800000

3. Create JPA Entities - User and Role (Many-to-Many Mapping)

In this step, we will create User and Role JPA entities and establish MANY-to-MANY relationships between them. Let's use JPA annotations to establish MANY-to-MANY relationships between User and Role entities.

User

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.util.Set;

@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    @Column(nullable = false, unique = true)
    private String username;
    @Column(nullable = false, unique = true)
    private String email;
    @Column(nullable = false)
    private String password;

    @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    @JoinTable(name = "users_roles",
        joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
            inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id")
    )
    private Set<Role> roles;
}

Role

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "roles")
public class Role {

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

4. Create Spring Data JPA Repositories

Next, let's create a repository package. Within a repository package, we create UserRepository and RoleRepository interfaces.
public interface UserRepository extends JpaRepository<User, Long> {
	Optional<User> findByUsernameOrEmail(String username, String email);
}

public interface RoleRepository extends JpaRepository<Role, Long> {

    Role findByName(String name);
}

5. JWT Implementation Classes

Create a security package for your spring boot project and add the following JWT-related classes.

JwtAuthenticationEntryPoint

AuthenticationEntryPoint is used by ExceptionTranslationFilter to commence an authentication scheme. It is the entry point to check if a user is authenticated and logs the person in or throws an exception (unauthorized).
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {

        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
    }
}

JwtTokenProvider

Let's create a Utility class named JwtTokenProvider which provides methods for generating, validating, and extracting information from JSON Web Tokens (JWTs) used for authentication in a Spring Boot application.
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.security.Key;
import java.util.Date;

@Component
public class JwtTokenProvider {

    @Value("${app.jwt-secret}")
    private String jwtSecret;

    @Value("${app-jwt-expiration-milliseconds}")
    private long jwtExpirationDate;

    // generate JWT token
    public String generateToken(Authentication authentication){

        String username = authentication.getName();

        Date currentDate = new Date();

        Date expireDate = new Date(currentDate.getTime() + jwtExpirationDate);

        String token = Jwts.builder()
                .subject(username)
                .issuedAt(new Date())
                .expiration(expireDate)
                .signWith(key())
                .compact();

        return token;
    }

    private Key key(){
        return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret));
    }

    // get username from JWT token
    public String getUsername(String token){

        return Jwts.parser()
                .verifyWith((SecretKey) key())
                .build()
                .parseSignedClaims(token)
                .getPayload()
                .getSubject();
    }

    // validate JWT token
    public boolean validateToken(String token){
            Jwts.parser()
                    .verifyWith((SecretKey) key())
                    .build()
                    .parse(token);
            return true;

    }
}

JwtAuthenticationFilter

Let's create a JwtAuthenticationFilter class in a Spring Boot application that intercepts incoming HTTP requests and validates JWT tokens that are included in the Authorization header. If the token is valid, the filter sets the current user's authentication in the SecurityContext.
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
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.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

// Execute Before Executing Spring Security Filters
// Validate the JWT Token and Provides user details to Spring Security for Authentication
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private JwtTokenProvider jwtTokenProvider;

    private UserDetailsService userDetailsService;

    public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider, UserDetailsService userDetailsService) {
        this.jwtTokenProvider = jwtTokenProvider;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        // Get JWT token from HTTP request
        String token = getTokenFromRequest(request);

        // Validate Token
        if(StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)){
            // get username from token
            String username = jwtTokenProvider.getUsername(token);

            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
              userDetails,
              null,
              userDetails.getAuthorities()
            );

            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }

        filterChain.doFilter(request, response);
    }

    private String getTokenFromRequest(HttpServletRequest request){
        String bearerToken = request.getHeader("Authorization");

        if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")){
            return bearerToken.substring(7, bearerToken.length());
        }

        return null;
    }
}

6. Spring Security Implementation

CustomUserDetailsService

Let's create a CustomUserDetailsService class that implements the UserDetailsService interface ( Spring security in-build interface) and provides an implementation for the loadUserByUername() method:
import lombok.AllArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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;

import java.util.Set;
import java.util.stream.Collectors;

@Service
@AllArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String usernameOrEmail) throws UsernameNotFoundException {

        User user = userRepository.findByUsernameOrEmail(usernameOrEmail, usernameOrEmail)
                .orElseThrow(() -> new UsernameNotFoundException("User not exists by Username or Email"));

        Set<GrantedAuthority> authorities = user.getRoles().stream()
                .map((role) -> new SimpleGrantedAuthority(role.getName()))
                .collect(Collectors.toSet());

        return new org.springframework.security.core.userdetails.User(
                usernameOrEmail,
                user.getPassword(),
                authorities
        );
    }
}

SpringSecurityConfig

Let's create a class SpringSecurityConfig and add the following configuration to it:
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableMethodSecurity
@AllArgsConstructor
public class SpringSecurityConfig {

    private UserDetailsService userDetailsService;

    private JwtAuthenticationEntryPoint authenticationEntryPoint;

    private JwtAuthenticationFilter authenticationFilter;

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

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http.csrf(csrf -> csrf.disable())
                .authorizeHttpRequests((authorize) -> {
//                    authorize.requestMatchers(HttpMethod.POST, "/api/**").hasRole("ADMIN");
//                    authorize.requestMatchers(HttpMethod.PUT, "/api/**").hasRole("ADMIN");
//                    authorize.requestMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN");
//                    authorize.requestMatchers(HttpMethod.GET, "/api/**").hasAnyRole("ADMIN", "USER");
//                    authorize.requestMatchers(HttpMethod.PATCH, "/api/**").hasAnyRole("ADMIN", "USER");
//                    authorize.requestMatchers(HttpMethod.GET, "/api/**").permitAll();
                    authorize.requestMatchers("/api/auth/**").permitAll();
                    authorize.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll();
                    authorize.anyRequest().authenticated();
                }).httpBasic(Customizer.withDefaults());

        http.exceptionHandling( exception -> exception
                .authenticationEntryPoint(authenticationEntryPoint));

        http.addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

//    @Bean
//    public UserDetailsService userDetailsService(){
//
//        UserDetails ramesh = User.builder()
//                .username("ramesh")
//                .password(passwordEncoder().encode("password"))
//                .roles("USER")
//                .build();
//
//        UserDetails admin = User.builder()
//                .username("admin")
//                .password(passwordEncoder().encode("admin"))
//                .roles("ADMIN")
//                .build();
//
//        return new InMemoryUserDetailsManager(ramesh, admin);
//    }
}

7. Creating DTO classes - LoginDto and JwtAuthResponse

Let's create a LoginDto class and add the following content to it:
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class LoginDto {
    private String usernameOrEmail;
    private String password;
}

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class JwtAuthResponse {
    private String accessToken;
    private String tokenType = "Bearer";
}

8. Creating a Service Layer

Create a service package and add the following service layer-related AuthService interface and AuthServiceImpl class.

AuthService

public interface AuthService {
    String login(LoginDto loginDto);
}

AuthServiceImpl

import lombok.AllArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import java.util.HashSet;
import java.util.Set;

@Service
@AllArgsConstructor
public class AuthServiceImpl implements AuthService {

    private AuthenticationManager authenticationManager;

    @Override
    public String login(LoginDto loginDto) {

        Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
                loginDto.getUsernameOrEmail(),
                loginDto.getPassword()
        ));

        SecurityContextHolder.getContext().setAuthentication(authentication);

        String token = jwtTokenProvider.generateToken(authentication);

        return token;
    }
}

9. Controller Layer - Login REST API return JWT Token

This code defines a REST API endpoint for user authentication. It receives a POST request at the "/api/auth/login" URL with the login credentials in the request body as a JSON object. The LoginDto object is used to map the JSON object to a Java object.
import lombok.AllArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@AllArgsConstructor
@RestController
@RequestMapping("/api/auth")
public class AuthController {

    private AuthService authService;

    // Build Login REST API
    @PostMapping("/login")
    public ResponseEntity<JwtAuthResponse> login(@RequestBody LoginDto loginDto){
        String token = authService.login(loginDto);

        JwtAuthResponse jwtAuthResponse = new JwtAuthResponse();
        jwtAuthResponse.setAccessToken(token);

        return new ResponseEntity<>(jwtAuthResponse, HttpStatus.OK);
    }

}

10. Insert SQL Scripts

Before testing Spring security and JWT, make sure that you use the SQL scripts below to insert the database into respective tables:

INSERT INTO `users` VALUES
(1,'ramesh@gmail.com','ramesh','$2a$10$5PiyN0MsG0y886d8xWXtwuLXK0Y7zZwcN5xm82b4oDSVr7yF0O6em','ramesh'),
(2,'admin@gmail.com','admin','$2a$10$gqHrslMttQWSsDSVRTK1OehkkBiXsJ/a4z2OURU./dizwOQu5Lovu','admin');

INSERT INTO `roles` VALUES (1,'ROLE_ADMIN'),(2,'ROLE_USER');

INSERT INTO `users_roles` VALUES (2,1),(1,2);

11. Testing using Postman

Refer to the screenshot below to test the Login REST API that returns the JWT token in the response for the Admin User:

12. Implement Role-Based Authorization

Create a Simple REST API and Secure it using Admin Role

@RestController
@RequestMapping("/api/")
public class AdminController {

    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping("/admin")
    public ResponseEntity<String> helloAdmin(){
        return ResponseEntity.ok("Hello Admin");
    }

    @PreAuthorize("hasRole('USER')")
    @GetMapping("/user")
    public ResponseEntity<String> helloUser(){
        return ResponseEntity.ok("Hello User");
    }
}
The above code demonstrates the role-based authorization for RESTful endpoints using Spring Security.

@PreAuthorize("hasRole('ADMIN')"): This is applied to the helloAdmin() method. It specifies that this endpoint can only be accessed by users who have the 'ADMIN'.

@GetMapping("/admin"): This maps HTTP GET requests to /api/admin to the helloAdmin() method. When accessed, it returns a response with "Hello Admin".

@PreAuthorize("hasRole('USER')"): This annotation on the helloUser() method restricts access to users with the 'USER' role. Spring Security enforces this role check before allowing access to the method.

@GetMapping("/user"): Maps HTTP GET requests to /api/user to the helloUser() method. Authorized users with the 'USER' role will receive a "Hello User" response.

Access REST APIs using the JWT Token

Next, pass the JWT token in the header to access admin-related REST APIs:

Conclusion

In this tutorial, we have implemented token-based authentication and role-based authorization using Spring Boot 3, Spring Security, JWT, and MySQL database. 

Comments

  1. package com.applephagna.jwtauthentication.service;

    import com.applephagna.jwtauthentication.repository.UserRepository;
    import lombok.AllArgsConstructor;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.User;
    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;

    import java.util.Set;
    import java.util.stream.Collectors;

    @Service
    @AllArgsConstructor
    public class CustomUserDetailsService implements UserDetailsService {

    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String usernameOrEmail) throws UsernameNotFoundException {

    User user = userRepository.findByUsernameOrEmail(usernameOrEmail, usernameOrEmail)
    .orElseThrow(() -> new UsernameNotFoundException("User not exists by Username or Email"));

    Set authorities = user.getRoles().stream()
    .map((role) -> new SimpleGrantedAuthority(role.getName()))
    .collect(Collectors.toSet());

    return new org.springframework.security.core.userdetails.User(
    usernameOrEmail,
    user.getPassword(),
    authorities
    );
    }
    }
    Cannot resolve method 'findByUsernameOrEmail' in 'UserRepository'
    Cannot resolve method 'getRoles' in 'User'
    Cannot resolve method 'getName()'
    how to fixed it.

    ReplyDelete
  2. Added missing 'findByUsernameOrEmail' to 'UserRepository. For getRoles and getName methods, you need to install Lombok plugin in your IDE.

    ReplyDelete
  3. I think you should add Some AuthenticationProvider like DaoAuthenticationProvider. Please correct me if i am wrong

    ReplyDelete

Post a Comment

Leave Comment