Building a Role and Permissions JWT-Based API with Spring Boot and MySQL

Building a Role and Permissions JWT-Based API with Spring Boot and MySQL

Java

Building a Role and Permissions JWT-Based API with Spring Boot and MySQL

Welcome to our blog! In this article, we will guide you through the process of building a secure and robust Role and Permissions API using JWT (JSON Web Token), Spring Boot, and a MySQL database. This powerful combination of technologies empowers you to create a flexible and scalable solution for efficiently managing user roles and permissions within your web application.

Prerequisites

Before we begin, ensure that you have the following components in place to successfully build and run the application:

  1. JDK 11

  2. Maven

  3. MySQL

Exploring the Role and Permissions API with JWT

Throughout this blog, we will delve into the implementation of a Role and Permissions API, harnessing the power of JWT for authentication and authorization. JSON Web Tokens serve as secure and compact mechanisms for exchanging information between parties in the form of a JSON object. They provide a dependable means of verifying the authenticity and permissions of users accessing our API.

By the end of this tutorial, you will have gained a comprehensive understanding of building a JWT-based API using Spring Boot and MySQL. Additionally, you will have the knowledge to effectively manage user roles and permissions within your application.

Let's dive in and embark on the journey of creating a secure and flexible Role and Permissions API using Spring Boot and MySQL!

Join us as we explore the inner workings of this powerful API implementation. Prepare to enhance your skills and elevate your application's security and functionality. Let's get started on our journey towards a secure and flexible Role and Permissions API!


Create Simple Spring boot

Let's Create Spring Boot Project from Spring Initializer site https://start.spring.io/

You can clone the project here

pom.xml

<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</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.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>com.auth0</groupId>
			<artifactId>java-jwt</artifactId>
			<version>4.2.2</version>
		</dependency>
	</dependencies>

Database connection properties

Add below properties in application.properties file.

spring.datasource.driver-class-name =com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/hustlerfundportal
spring.datasource.username=root
spring.datasource.password=mysql
spring.jpa.show-sql=true
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.properties.hibernate.formart_sql=true
jwt.expiration=1800000
jwt.secret=mySecret
jwt.refreshToken.expiration=120
logging.level.org.springframework.security=DEBUG

Create Entity JwtUser 

@EqualsAndHashCode(of = "uuid")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class JwtUser implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Builder.Default
    private String uuid = UUID.randomUUID().toString();
    @Column
    private String username;
    @Column(unique = true)
    private String email;
    @Column
    private String password;
    @Column
    @Enumerated(EnumType.STRING)
    @ElementCollection(fetch = FetchType.EAGER)
    private Set<Role.Permissions> permissions = new HashSet<>();
    @Column
    @Builder.Default
    private boolean enabled = false;
    @OneToOne(mappedBy = "user")
    private RefreshToken refreshToken;
    @Column
    private String roleName;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Set<GrantedAuthority> authorities = new HashSet<>();
        for (var r : this.permissions) {
            var sga = new SimpleGrantedAuthority(r.name());
            authorities.add(sga);
        }
        return authorities;
    }
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    @Override
    public boolean isEnabled() {
        return this.enabled;
    }

Create Entity RefreshToken 

@EqualsAndHashCode(of = "uuid")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class RefreshToken {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Builder.Default
    private String uuid = UUID.randomUUID().toString();
    @Column
    private String token;
    @Column
    private ZonedDateTime expiration;
    @OneToOne
    @JoinColumn(nullable = false, name = "user_id")
    private JwtUser user;
}

Create Entity Role 


@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(unique = true)
    private String roleName;
    @ElementCollection(targetClass = Permissions.class,fetch = FetchType.EAGER)
    @Enumerated(EnumType.STRING)
    private List<Permissions> permissions;
    public enum Permissions {
        ROLE_USER,
        ROLE_ADMIN,
        ROLE_API
    }
    public List<Permissions> getPermissions() {
        return this.permissions;
    }
    public void setPermissions(List<Permissions> permissions) {
        this.permissions = permissions;
    }
}

JPA Repository (Data Layer)

JPARepository is an interface that provides a generic way to access data stored in a database. It extends CrudRepository, which provides basic CRUD operations, and adds additional methods for more complex operations, such as flushing the persistent context and deleting records in a batch.

In the given code, there are three JPA repositories:

  1. RefreshTokenRepository:
java
@Repository public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> { Optional<RefreshToken> findRefreshTokenByToken(String token); }

This repository is responsible for interacting with the RefreshToken entity in the database. It extends the JpaRepository interface and provides a method findRefreshTokenByToken to find a refresh token by its token value.

  1. JwtUserRepository:
java
@Repository public interface JwtUserRepository extends JpaRepository<JwtUser, Long> { JwtUser findJwtUserByUsername(String username); JwtUser findJwtUserByEmail(String email); }

This repository is responsible for interacting with the JwtUser entity in the database. It extends the JpaRepository interface and provides methods findJwtUserByUsername and findJwtUserByEmail to find a JwtUser entity by username and email, respectively.

  1. RoleRepository:
java
public interface RoleRepository extends JpaRepository<Role, Long> { Optional<Role> findByRoleName(String roleName); }

This repository is responsible for interacting with the Role entity in the database. It extends the JpaRepository interface and provides a method findByRoleName to find a role entity by its roleName.

These repositories allow you to perform CRUD (Create, Read, Update, Delete) operations and other custom queries on the corresponding entities using the methods provided by JpaRepository. Spring Data JPA automatically generates the implementation for these methods based on the method names and entity types.

Web Security Configuration

The SecurityBeans class serves the purpose of providing essential security-related beans and configurations for a Spring Boot application. It is responsible for defining and creating two beans: passwordEncoder(), which returns an instance of BCryptPasswordEncoder used for password hashing, and authenticationManager(), which retrieves the authentication manager configured in the Spring Security framework. By utilizing these beans, the class contributes to setting up the necessary components for password encoding and authentication management within the application's Spring Security configuration.

@Component
public class SecurityBeans {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @SneakyThrows
    @Bean
    public AuthenticationManager authenticationManager
(AuthenticationConfiguration authenticationConfiguration) {
        return authenticationConfiguration.getAuthenticationManager();
    }
}

The AuthSuccessHandler class is an essential component in a Spring Security-based application, responsible for handling the successful authentication process. By extending the SimpleUrlAuthenticationSuccessHandler class, it overrides the onAuthenticationSuccess method to define custom behavior. Upon successful authentication, this class retrieves the details of the authenticated user and generates a JSON Web Token (JWT) using the JwtUtils class. It also creates a refresh token using the RefreshTokenService. The class then adds the JWT to the response headers as the "Authorization" field, along with the appropriate "Content-Type" header. Finally, it writes the JWT and refresh token as a JSON response using the ObjectMapper class. In summary, the AuthSuccessHandler class plays a vital role in managing authentication success, providing the necessary tokens and response information for further authentication and authorization in the application.

@Component
@Slf4j
@RequiredArgsConstructor
public class AuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
    private final ObjectMapper objectMapper = new ObjectMapper();
    @Autowired
    JwtUserRepository jwtUserRepository;
    private final JwtUtils jwtUtils;
    private final RefreshTokenService refreshTokenService;
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) 
throws IOException, ServletException {
        UserDetails principal = (UserDetails) authentication.getPrincipal();
        var user = jwtUserRepository.findJwtUserByUsername(principal.getUsername());
        String token = jwtUtils.createJwt(user.getEmail(),
user.getUsername(),
user.getRoleName(),
user.getAuthorities());
        String refreshToken = refreshTokenService.createToken(user);
        response.addHeader("Authorization", "Bearer " + token);
        response.addHeader("Content-Type", "application/json");
        response.getWriter().write(objectMapper.writeValueAsString
(JwtResponseDto.of(token, refreshToken)));
    }
}

The JsonObjectAuthenticationFilter class is a custom filter used in a Spring Security-based application to handle authentication requests where the credentials are provided as a JSON object in the request body. It extends the UsernamePasswordAuthenticationFilter class and overrides the attemptAuthentication method. Within this method, the class reads the request body and converts it into a LoginCredentials object using the ObjectMapper class. It then creates an UsernamePasswordAuthenticationToken with the email and password from the LoginCredentials object. After setting the authentication details, it delegates the authentication process to the AuthenticationManager and returns the result. If an error occurs during the authentication attempt, it logs an error message and throws a RuntimeException. This class provides a convenient way to handle authentication requests with JSON credentials and integrates seamlessly into the Spring Security framework.

@Slf4j
public class JsonObjectAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private final ObjectMapper objectMapper = new ObjectMapper();
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
 HttpServletResponse response) {
        try {
            BufferedReader reader = request.getReader();
            StringBuilder sb = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
            LoginCredentials authRequest = 
objectMapper.readValue(sb.toString(), LoginCredentials.class);
            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
                    authRequest.getEmail(), authRequest.getPassword()
            );
            log.info("Testing Here  ####################################");
            setDetails(request, token);
            return this.getAuthenticationManager().authenticate(token);
        } catch (IOException e) {
            log.info("Failed Here  ####################################"+e+"");
            throw new RuntimeException(e);
        }
    }
}

The JwtAuthorizationFilter class is a custom filter used in a Spring Security-based application to handle authorization based on JWT (JSON Web Token) authentication. It extends the BasicAuthenticationFilter class and overrides the doFilterInternal method. This filter intercepts requests and validates the JWT token present in the request header.

Overall, the JwtAuthorizationFilter is responsible for validating and authenticating JWT tokens in incoming requests, enabling secure authorization for protected endpoints in the application.

public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
    private static final String TOKEN_PREFIX = "Bearer ";
    private final JwtUserDetailsService jwtUserDetailsService;
    private final String secret;
    public JwtAuthorizationFilter(AuthenticationManager authenticationManager,
 JwtUserDetailsService jwtUserDetailsService, String secret) {
        super(authenticationManager);
        this.jwtUserDetailsService = jwtUserDetailsService;
        this.secret = secret;
    }
    @Override
    protected  void doFilterInternal(HttpServletRequest request,
 HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        UsernamePasswordAuthenticationToken auth = getAuthentication(request);
        if (auth==null) {
            filterChain.doFilter(request, response);
            return;
        }
        SecurityContextHolder.getContext().setAuthentication(auth);
        filterChain.doFilter(request, response);
    }
    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        String token = request.getHeader(HttpHeaders.AUTHORIZATION);
        if (token == null || !token.startsWith(TOKEN_PREFIX)) {
            return null;
        }
        String email = JWT.require(Algorithm.HMAC256(secret))
                .build()
                .verify(token.replace(TOKEN_PREFIX, ""))
                .getSubject();
        if (email == null) return null;
        UserDetails userDetails = jwtUserDetailsService.loadUserByUsername(email);
        return new UsernamePasswordAuthenticationToken(userDetails.getUsername(),
 null, userDetails.getAuthorities());
    }
}


The JwtCustomSecurity class is a configuration class that sets up the security configuration for JWT-based authentication in a Spring Security-enabled application. It defines the authentication and authorization rules, creates the necessary filters, and establishes the security filter chain.

Overall, the JwtCustomSecurity class provides a central configuration for JWT-based authentication, defines the authorization rules, and sets up the necessary filters in the security filter chain for secure and controlled access to the application endpoints.

@Configuration
public class JwtCustomSecurity {
    @Autowired
    private AuthenticationManager authenticationManager;
    private final AuthSuccessHandler authSuccessHandler;
    private final JwtUserDetailsService jwtUserDetailsService;
    private final String secret;
    public JwtCustomSecurity(AuthSuccessHandler authSuccessHandler, 
JwtUserDetailsService jwtUserDetailsService, @Value("${jwt.secret}") String secret) {
        this.authSuccessHandler = authSuccessHandler;
        this.jwtUserDetailsService = jwtUserDetailsService;
        this.secret = secret;
    }
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .cors()
                .and()
                .csrf()
                .disable()
                .authorizeHttpRequests((auth) -> {
                    try {
                        auth
                      .antMatchers("/user").hasRole("USER")
                     .antMatchers("/admin").hasRole("ADMIN")
                    .antMatchers("/api").hasRole("API")
                   .anyRequest().permitAll()
                 .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
              .addFilter(authenticationFilter())
   .addFilter(new JwtAuthorizationFilter(authenticationManager, jwtUserDetailsService, secret))
                                .exceptionHandling()
   .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                })
                .httpBasic(Customizer.withDefaults());
        return http.build();
    }
    @Bean
    public JsonObjectAuthenticationFilter authenticationFilter() throws Exception {
        JsonObjectAuthenticationFilter filter = new JsonObjectAuthenticationFilter();
        filter.setAuthenticationSuccessHandler(authSuccessHandler);
        filter.setAuthenticationManager(authenticationManager);
        return filter;
    }
}


The JwtUtils class is a component responsible for generating JSON Web Tokens (JWTs) in a Spring application. It uses the Algorithm.HMAC256 algorithm to sign the tokens with a secret key.

Overall, the JwtUtils class provides a convenient utility for creating JWTs with the necessary claims and signing them using a secret key. It plays a crucial role in the authentication and authorization process of a Spring application that uses JWT-based authentication.


@Component
@RequiredArgsConstructor
public class JwtUtils {
    @Value("${jwt.expiration}")
    private int expTime;
    @Value("${jwt.secret}")
    private String secret;
    public String createJwt(String email, String username, 
String roleName, Collection<? extends GrantedAuthority> authorities) {
        // Convert authorities to a list of strings
        List<String> authorityStrings = authorities.stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList());
        return JWT.create()
                .withSubject(email)
                .withClaim("username",username)
                .withClaim("role",roleName)
                .withClaim("authorities", authorityStrings)
.withExpiresAt(Instant.ofEpochMilli(ZonedDateTime.now(ZoneId.systemDefault())
.toInstant().toEpochMilli() + expTime))
                .sign(Algorithm.HMAC256(secret));
    }
}

The JwtRefreshRequestDto class is a data transfer object (DTO) used to represent a refresh token request in a Spring application.

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class JwtRefreshRequestDto {
    private String refreshToken;
}

The JwtResponseDto class is a data transfer object (DTO) used to represent a response containing a JWT (access token) and a refresh token in a Spring application.

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class JwtResponseDto {
    private String token;
    private String refreshToken;
    public static JwtResponseDto of(String token, String refreshToken) {
        return new JwtResponseDto(token, refreshToken);
    }
}


The LoginCredentials class is a data class used to represent the login credentials of a user in a Spring application.

By using this LoginCredentials class, it becomes convenient to encapsulate and transfer the user's login credentials as a single object within the application. It helps to organize and handle the authentication process more efficiently.

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginCredentials {
    private String email;
    private String password;
}

Controller

The AuthController defines a single endpoint refreshJwt mapped to the POST HTTP method and "/refresh" path. It takes a JwtRefreshRequestDto object as the request body, which represents the refresh token request. The method delegates the token refreshing logic to the refreshTokenService by calling its refreshToken method. It expects the refreshTokenService to return a JwtResponseDto object containing the new access token and refresh token. This response is then returned as the HTTP response from the endpoint.

By using this AuthController, it becomes possible to handle refresh token requests and obtain a new access token in a secure manner. The controller acts as a bridge between the client's refresh token request and the refreshTokenService that performs the actual token refreshing process.

@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {
    private final RefreshTokenService refreshTokenService;
    @PostMapping("/refresh")
    public JwtResponseDto refreshJwt(@RequestBody JwtRefreshRequestDto refreshRequestDto) {
        return refreshTokenService.refreshToken(refreshRequestDto);
    }
}


The RoleController class is a REST controller class responsible for handling requests related to roles and permissions in a Spring application.

The class has three endpoints:

  1. createRole: This endpoint is mapped to the POST HTTP method and does not have any additional path. It expects a Role object as the request body, representing the role to be created. The method saves the role in the roleRepository and returns a ResponseEntity with the created role in the response body.

  2. getPermissionsByRoleName: This endpoint is mapped to the GET HTTP method and expects a roleName path variable. It retrieves the role from the roleRepository based on the provided role name. If the role is found, it returns a ResponseEntity with the permissions associated with that role in the response body. If the role is not found, it returns a 404 Not Found response.

  3. getAllPermissions: This endpoint is mapped to the GET HTTP method and does not have any additional path. It returns a ResponseEntity with all the available permissions in the Role.Permissions enum in the response body.


@RestController
@RequestMapping("/roles")
public class RoleController {
    @Autowired
    private RoleRepository roleRepository;
    @PostMapping
    public ResponseEntity<Role> createRole(@RequestBody Role role) {
        Role savedRole = roleRepository.save(role);
        return ResponseEntity.created(null).body(savedRole);
    }
    @GetMapping("/{roleName}/permissions")
    public ResponseEntity<List<Role.Permissions>> 
getPermissionsByRoleName(@PathVariable String roleName) {
        Optional<Role> roleOptional = roleRepository.findByRoleName(roleName);
        if (roleOptional.isPresent()) {
            Role role = roleOptional.get();
            return ResponseEntity.ok(role.getPermissions());
        }
        return ResponseEntity.notFound().build();
    }
    @GetMapping("/permissions")
    public ResponseEntity<List<Role.Permissions>> getAllPermissions() {
        return ResponseEntity.ok(Arrays.asList(Role.Permissions.values()));
    }
}

The TestController class is a REST controller class that defines three endpoints for testing purposes in a Spring application.

The class has three endpoints:

  1. userEndpoint: This endpoint is mapped to the GET HTTP method and is accessible under the "/user" path. It returns a TestMessage object with the message "Hello user!" in the response body.

  2. adminEndpoint: This endpoint is mapped to the GET HTTP method and is accessible under the "/admin" path. It returns a TestMessage object with the message "Hello admin!" in the response body.

  3. apiEndpoint: This endpoint is mapped to the GET HTTP method and is accessible under the "/api" path. It returns a TestMessage object with the message "Hello Api!" in the response body.

@RestController
public class TestController {
    @GetMapping("/user")
    public TestMessage userEndpoint() {
        return new TestMessage("Hello user!");
    }
    @GetMapping("/admin")
    public TestMessage adminEndpoint() {
        return new TestMessage("Hello admin!");
    }
    @GetMapping("/api")
    public TestMessage apiEndpoint() {
        return new TestMessage("Hello Api!");
    }
}


The TestMessage class, it becomes possible to create instances of this class with a specific message and utilize them in various parts of the application. It is a lightweight class used to encapsulate and pass around test messages within the application.

@Value
public class TestMessage {
    String msg;
}

Initializing Users and Roles at Application Startup

The InitUsers class is a Spring component that implements the CommandLineRunner interface, allowing it to run specific code when the application starts up. It is responsible for initializing users and roles in the system.

This class ensures that the necessary roles and users are available in the system during application startup, providing a foundation for authentication and authorization functionality.

@Component
@RequiredArgsConstructor
public class InitUsers implements CommandLineRunner {
    private final PasswordEncoder passwordEncoder;
    @Autowired
    JwtUserRepository jwtUserRepository;
    @Autowired
    RoleRepository roleRepository;
    @Override
    public void run(String... args) throws Exception {
        String role1 ="ADMINISTRATOR_ROLE";
        String role2 ="API_ROLE";
        Optional<Role> roleAdmin = roleRepository.findByRoleName(role1);
        Optional<Role> roleAPI = roleRepository.findByRoleName(role2);
        if (roleAdmin.isEmpty()) {
            Role adminRole = new Role();
            adminRole.setRoleName(role1);
            adminRole.setPermissions(Arrays.asList(Role.Permissions.ROLE_ADMIN,
 Role.Permissions.ROLE_USERMGMT));
            roleRepository.save(adminRole);
        }
        if (roleAPI.isEmpty()) {
            Role apiRole = new Role();
            apiRole.setRoleName(role2);
            apiRole.setPermissions(Arrays.asList(Role.Permissions.ROLE_API, 
Role.Permissions.ROLE_USERMGMT));
            roleRepository.save(apiRole);
        }
        JwtUser jwtUser = jwtUserRepository.findJwtUserByEmail("admin@test.com");
        JwtUser jwtUser2 = jwtUserRepository.findJwtUserByEmail("apiuser@test.com");
        if (jwtUser==null) {
            Optional<Role>  perms = roleRepository.findByRoleName(role1);
            Role role = perms.get();
            List<Role.Permissions> permissions = role.getPermissions();
            JwtUser u = JwtUser.builder()
                    .username("Admin")
                    .email("admin@test.com")
                    .password(passwordEncoder.encode("test123"))
                    .enabled(true)
                    .roleName(role1)
                    .permissions(new HashSet<>(permissions))
                    .build();
        jwtUserRepository.save(u);
        }
        if (jwtUser2==null) {
            Optional<Role>  perms = roleRepository.findByRoleName(role2);
            Role role = perms.get();
            List<Role.Permissions> permissions = role.getPermissions();
            JwtUser u = JwtUser.builder()
                    .username("api")
                    .email("apiuser@test.com")
                    .password(passwordEncoder.encode("test123"))
                    .enabled(true)
                    .roleName(role2)
                    .permissions(new HashSet<>(permissions))
                    .build();
            jwtUserRepository.save(u);
        }
    }
}

User Details Service for JWT Authentication

The JwtUserDetailsService class is a Spring service that implements the UserDetailsService interface. It is responsible for loading user details from the JwtUserRepository based on the provided email.

The class is annotated with @Service, indicating that it is a Spring-managed service. It also uses the @RequiredArgsConstructor annotation to generate a constructor that injects the required dependencies.

Inside the loadUserByUsername method, the user details are retrieved from the JwtUserRepository by searching for a user with the specified email. The retrieved JwtUser object is then used to create and return an instance of UserDetails. This includes information such as the username, password, enabled status, and authorities (roles and permissions) associated with the user.

This service plays a crucial role in the authentication process, as it allows Spring Security to retrieve user details based on the provided email during authentication and authorization operations.

@Service
@RequiredArgsConstructor
public class JwtUserDetailsService implements UserDetailsService {
    @Autowired
    JwtUserRepository jwtUserRepository;
    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        JwtUser user = jwtUserRepository.findJwtUserByEmail(email);
        return new User(user.getUsername(), user.getPassword(), user.isEnabled(),
true, true, true, user.getAuthorities());
    }
}

The RefreshTokenService class is a Spring service that manages refresh tokens for JWT authentication. It provides methods for creating and refreshing tokens. It generates a new token with a unique value and expiration time when requested and refreshes existing tokens if they are valid and not expired. This service ensures secure and continuous user authentication in a JWT-based system.

@Service
@RequiredArgsConstructor
public class RefreshTokenService {
    private final RefreshTokenRepository refreshTokenRepository;
    private final JwtUtils jwtUtils;
    @Value("${jwt.refreshToken.expiration}")
    private int expiration;
    public String createToken(JwtUser user) {
        var refreshToken = refreshTokenRepository.save(RefreshToken.builder()
                .token(UUID.randomUUID().toString())
                .user(user)
                .expiration(ZonedDateTime.now(ZoneId.systemDefault()).plusMinutes(expiration))
                .build());
        return refreshToken.getToken();
    }
    public JwtResponseDto refreshToken(JwtRefreshRequestDto refreshRequestDto) {
        var tokenOpt = 
refreshTokenRepository.findRefreshTokenByToken(refreshRequestDto.getRefreshToken());
        if (tokenOpt.isEmpty()) {
            throw new RuntimeException
("Refresh Token %s not found!"+refreshRequestDto.getRefreshToken());
        }
        var token = tokenOpt.get();
        if (isTokenExpired(token.getExpiration())) {
            refreshTokenRepository.delete(token);
            throw new RuntimeException
("Refresh Token %s was expired!"+refreshRequestDto.getRefreshToken());
        }
        String jwt = jwtUtils.createJwt
(token.getUser().getEmail(),token.getUser().getUsername(),
token.getUser().getRoleName(),token.getUser().getAuthorities());
        updateToken(token);
        return JwtResponseDto.of(jwt, token.getToken());
    }
    private void updateToken(RefreshToken token) {
        token.setExpiration(ZonedDateTime.now(ZoneId.systemDefault()).plusMinutes(expiration));
        refreshTokenRepository.save(token);
    }
    private boolean isTokenExpired(ZonedDateTime expirationTime) {
        return expirationTime.isBefore(ZonedDateTime.now(ZoneId.systemDefault()));
    }
}

Testing

Now finally will test this by using below steps:

Endpoint: POST /login URL: http://localhost:8080/login

Request Body:

json
{ "email": "api@test.com", "password": "test123" }

Response Body:

json
{ "token": "sample_new_token", "refreshToken": "sample_refresh_token" }

  1. Endpoint: /auth/refresh

    • Method: POST
    • Description: Refreshes the JWT token using a refresh token.
    • Request Body (JSON):
      json
      { "refreshToken": "sample_refresh_token" }
    • Response (JSON):
      json
      { "token": "sample_new_token", "refreshToken": "sample_refresh_token" }
  2. Endpoint: /roles

    • Method: POST
    • Description: Creates a new role.
    • Request Body (JSON):
      json
      { "roleName": "sample_role_name", "permissions": ["PERMISSION_1", "PERMISSION_2"] }
    • Response (JSON):
      json
      { "id": 1, "roleName": "sample_role_name", "permissions": ["PERMISSION_1", "PERMISSION_2"] }
  3. Endpoint: /roles/{roleName}/permissions

    • Method: GET
    • Description: Retrieves the permissions associated with a specific role.
    • Path Parameter:
      • {roleName}: The name of the role to retrieve permissions for.
    • Response (JSON):
      json
      ["PERMISSION_1", "PERMISSION_2"]
  4. Endpoint: /roles/permissions

    • Method: GET
    • Description: Retrieves all available permissions.
    • Response (JSON):
      json
      ["PERMISSION_1", "PERMISSION_2", "PERMISSION_3"]
  5. Endpoint: /user

    • Method: GET
    • Description: Returns a test message for the user role.
    • Response (JSON):
      json
      { "msg": "Hello user!" }
  6. Endpoint: /admin

    • Method: GET
    • Description: Returns a test message for the admin role.
    • Response (JSON):
      json
      { "msg": "Hello admin!" }
  7. Endpoint: /api

    • Method: GET
    • Description: Returns a test message for the API role.
    • Response (JSON):
      json
      { "msg": "Hello API!" }

Please note that the JSON request and response structures provided are simplified for demonstration purposes and may vary depending on the actual implementation.

Download Source Code


Building a Role and Permissions JWT-Based API with Spring Boot and MySQL

We are what we repeatedly do. Excellence then, is not an act, but a habit

June 06, 2023

0
0

Comments

+

© 2024 Inc. All rights reserved. mulikevs