Building a Role and Permissions JWT-Based API with Spring Boot and MySQL
JavaBuilding 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:
JDK 11
Maven
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>
<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:
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.
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.
RoleRepository
:
javapublic 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:
createRole
: This endpoint is mapped to thePOST
HTTP method and does not have any additional path. It expects aRole
object as the request body, representing the role to be created. The method saves the role in theroleRepository
and returns aResponseEntity
with the created role in the response body.getPermissionsByRoleName
: This endpoint is mapped to theGET
HTTP method and expects aroleName
path variable. It retrieves the role from theroleRepository
based on the provided role name. If the role is found, it returns aResponseEntity
with the permissions associated with that role in the response body. If the role is not found, it returns a404 Not Found
response.getAllPermissions
: This endpoint is mapped to theGET
HTTP method and does not have any additional path. It returns aResponseEntity
with all the available permissions in theRole.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:
userEndpoint
: This endpoint is mapped to theGET
HTTP method and is accessible under the "/user" path. It returns aTestMessage
object with the message "Hello user!" in the response body.adminEndpoint
: This endpoint is mapped to theGET
HTTP method and is accessible under the "/admin" path. It returns aTestMessage
object with the message "Hello admin!" in the response body.apiEndpoint
: This endpoint is mapped to theGET
HTTP method and is accessible under the "/api" path. It returns aTestMessage
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);
}
}
}
@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" }
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" }
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"] }
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"]
Endpoint: /roles/permissions
- Method: GET
- Description: Retrieves all available permissions.
- Response (JSON):
json["PERMISSION_1", "PERMISSION_2", "PERMISSION_3"]
Endpoint: /user
- Method: GET
- Description: Returns a test message for the user role.
- Response (JSON):
json{ "msg": "Hello user!" }
Endpoint: /admin
- Method: GET
- Description: Returns a test message for the admin role.
- Response (JSON):
json{ "msg": "Hello admin!" }
Endpoint: /api
- Method: GET
- Description: Returns a test message for the API role.
- Response (JSON):
json{ "msg": "Hello API!" }
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" }
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"] }
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"]
Endpoint: /roles/permissions
- Method: GET
- Description: Retrieves all available permissions.
- Response (JSON):json
["PERMISSION_1", "PERMISSION_2", "PERMISSION_3"]
Endpoint: /user
- Method: GET
- Description: Returns a test message for the user role.
- Response (JSON):json
{ "msg": "Hello user!" }
Endpoint: /admin
- Method: GET
- Description: Returns a test message for the admin role.
- Response (JSON):json
{ "msg": "Hello admin!" }
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
June 06, 2023