I'm working on a Spring Boot app with Spring Security and MongoDB, but I'm having trouble getting user roles from the database to display in the UserDetails object. The roles field is always empty, despite confirming the roles are stored properly. When logging in, the authentication succeeds but the roles are not returned. I suspect there is an issue with how the roles are being added to the UserDetails object, but I'm unsure how to resolve it. I've tried various methods including manually adding the roles and checking logs, but I suspect the issue may be related to the mapping of User and UserRole entities in my MongoDB schema.
CustomAuthorizationFilter class
@Slf4j
public class CustomAuthorizationFilter extends OncePerRequestFilter {
private EnvironmentKey environmentKey;
private UserService userService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (request.getServletPath().equals("/api/v1/login")|| request.getServletPath().equals("/api/v1/token/refresh")) {
filterChain.doFilter(request,response);
}else {
String authorizationHeader= request.getHeader(AUTHORIZATION);
log.info("Authorization header {}",authorizationHeader);
String secretKey = System.getenv("MY_APP_SECRET_KEY");
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
try {
String token = authorizationHeader.substring("Bearer ".length());
Algorithm algorithm = Algorithm.HMAC256(secretKey.getBytes());
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT decodedJWT = verifier.verify(token);
String username = decodedJWT.getSubject();
// here is where i get the nullPointer Exception
String[] roles = decodedJWT.getClaim("roles").asArray(String.class);
log.info(String.valueOf(roles));
Collection<SimpleGrantedAuthority> authorityCollection = new ArrayList<>();
stream(roles).forEach(role -> {
authorityCollection.add(new SimpleGrantedAuthority(role));
});
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(username,null,authorityCollection);
SecurityContextHolder.getContext()
.setAuthentication(authenticationToken);
filterChain.doFilter(request,response);
}catch (JWTVerificationException ex) {
log.error("Error logging in: {}", ex.getMessage());
response.setHeader("ERROR", ex.getMessage());
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
Map<String,String> error = new HashMap<>();
error.put("error message",ex.getMessage());
response.setContentType(APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getOutputStream(),error);
}
}else {
log.info("Authorization header {}" ,authorizationHeader);
log.info("start do filter");
filterChain.doFilter(request,response);
}
}
}
}
UserSerivce class
@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class UserServiceImpl implements UserService, UserDetailsService {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
// Sanitize input by checking if the input is null or empty
if (email == null || email.isEmpty()) {
throw new IllegalArgumentException("email cannot be null or empty");
}
log.info("Looking up user with email: {}", email);
AppUser appUser = userRepository.findByEmailIgnoreCase(email);
if (appUser == null) {
throw new UsernameNotFoundException("email not found in the database");
}
log.info("Found user: {}", appUser);
Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
appUser.getRoles().forEach(userRole -> {
log.info("Adding role: {}", userRole.getName());
authorities.add(new SimpleGrantedAuthority(userRole.getName()));
});
return new org.springframework.security.core.userdetails.User(appUser.getEmail(), appUser.getPassword(), authorities);
}
Edit AppUser class
@Document(collection = "app_users")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AppUser {
@Id
private ObjectId id;
@NotBlank(message = "Name is mandatory")
private String userName;
@NotBlank(message = "Email is mandatory")
@Email(message = "Email should be valid")
private String email;
@NotBlank(message = "Password is mandatory")
private String password;
@DocumentReference
private Collection<UserRole> roles = new ArrayList<>();
public Collection<UserRole> getRoles() {
return roles;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("AppUser{")
.append("id=").append(id)
.append(", userName='").append(userName).append('\'')
.append(", email='").append(email).append('\'')
.append(", password='").append(password).append('\'')
.append(", roles=[");
for (UserRole role : roles) {
sb.append(role.getName()).append(", ");
}
if (!roles.isEmpty()) {
sb.setLength(sb.length() - 2); // Remove the last ", " characters
}
sb.append("]}");
return sb.toString();
}
}
UserRole class
@Document(collection = "user_roles")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRole {
@Id
private ObjectId id;
private String name;
public String getName() {
return name;
}
}
Security configuration class
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final UserDetailsService userDetailsService;
private final BCryptPasswordEncoder cryptPasswordEncoder;
private final AuthenticationConfiguration authenticationConfiguration;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
AbstractAuthenticationProcessingFilter filter = new CustomAuthenticationFilter(authenticationManager());
filter.setFilterProcessesUrl("/api/v1/login");
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeHttpRequests().requestMatchers("/api/v1/login/**","/api/token/refresh/**").permitAll();
http.authorizeHttpRequests().requestMatchers(GET, "/api/v1/user/**").permitAll();
http.authorizeHttpRequests().requestMatchers(GET, "/api/v1/user/**","/api/v1/reviews").hasAnyAuthority("ROLE_USER");
http.authorizeHttpRequests().requestMatchers(POST,"api/v1/movies/**","/api/v1/reviews/**","api/v1/user/**").hasAuthority("ROLE_ADMIN");
http.authorizeHttpRequests().requestMatchers(PUT,"api/v1/movies/**","/api/v1/reviews/**","api/v1/user/**").hasAuthority("ROLE_ADMIN");
http.authorizeHttpRequests().requestMatchers(DELETE,"api/v1/movies/**","/api/v1/reviews/**","api/v1/user/**").hasAuthority("ROLE_SUPER_ADMIN");
http.authorizeHttpRequests().anyRequest().authenticated();
http.addFilter(filter);
http.addFilterBefore(new CustomAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
I was able to resolve the issue by adding the roles to the MongoDB database, using the roleRepository.insert method to create a new UserRole object for the role and then add the role name to the user's roles array using the mongoTemplate.update method.
Here's the updated code for the addRoleToUser method:
@Override
public void addRoleToUser(String email, String roleName) {
// Sanitize input by checking if the input strings are null or empty
if (email == null || email.isEmpty() || roleName == null || roleName.isEmpty()) {
throw new IllegalArgumentException("Username and role name cannot be null or empty");
}
// Create a new UserRole object for the role
UserRole userRole = roleRepository.insert(new UserRole(roleName));
// Add the role name to the user's roles array using the mongoTemplate.update method
mongoTemplate.update(AppUser.class)
.matching(Criteria.where("email").is(email))
.apply(new Update().push("roles").value(roleName))
.first();
// Add the UserRole object to the user's roles list
AppUser user = userRepository.findByEmailIgnoreCase(email);
user.getRoles().add(userRole);
log.info("Adding new role {} to a user {}", roleName, email);
}