1

I'm building a RESTful API using Spring Boot and Spring Security. I have configured the security filters in my WebSecurityConfigurerAdapter to permit access to certain endpoints based on the user roles. However, when I test my endpoints with different users, I'm finding that requests are not being authorized correctly.

Here's the code for my security filter chain:



    @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/movies/**","/api/v1/reviews").hasAnyAuthority("ROLE_USER");
        http.authorizeHttpRequests().requestMatchers(GET, "/api/v1/movies/**","/api/v1/reviews","/api/v1/user").hasAnyAuthority("ROLE_ADMIN");
        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();

    }

Here is the authorization filter class


    @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();
                    String[] roles = decodedJWT.getClaim("roles").asArray(String.class);
                    Collection<SimpleGrantedAuthority> authorityCollections = new ArrayList<>();
                    stream(roles).forEach(role-> {
                        authorityCollections.add(new SimpleGrantedAuthority(role));

                    });

                    UsernamePasswordAuthenticationToken authenticationToken = new
                            UsernamePasswordAuthenticationToken(username,null
                            ,authorityCollections);

                    SecurityContextHolder.getContext()
                            .setAuthentication(authenticationToken);


                    filterChain.doFilter(request,response);



                }catch (JWTVerificationException ex) {
                    log.info("lets get an exception already");
                    log.info("Error logging in: {}", ex.getMessage());
                    response.setHeader("error", ex.getMessage());
                    response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                    //response.sendError(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);
            }
        }
    }
}

When I make requests to endpoints that should only be accessible to users with certain roles, I'm receiving a 403 Forbidden response. I have confirmed that the users I'm testing with have the correct roles assigned to them in my database.

Edit

When I turned on the security debug logging this is what i got

2023-04-26T17:36:44.776+02:00  INFO 4348 --- [nio-8080-exec-3] d.b.m.filter.CustomAuthorizationFilter   : Authorization header Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhcm5vbGRAZ21haWwuY29tIiwiZXhwIjoxNjgyNTI0Mjc0LCJpc3MiOiIvYXBpL3YxL2xvZ2luIiwicm9sZXMiOltdfQ._11bP_6XwvOX1upcRM8WhvXf6KdQQSMrlW3405VgjBM
2023-04-26T17:36:44.847+02:00 DEBUG 4348 --- [nio-8080-exec-3] o.s.s.w.access.AccessDeniedHandlerImpl   : Responding with 403 status code
2023-04-26T17:36:44.852+02:00 DEBUG 4348 --- [nio-8080-exec-3] o.s.security.web.FilterChainProxy        : Securing GET /error
2023-04-26T17:36:44.860+02:00 DEBUG 4348 --- [nio-8080-exec-3] o.s.s.w.a.AnonymousAuthenticationFilter  : Set SecurityContextHolder to anonymous SecurityContext
2023-04-26T17:36:44.861+02:00 DEBUG 4348 --- [nio-8080-exec-3] o.s.s.w.a.Http403ForbiddenEntryPoint     : Pre-authenticated entry point called. Rejecting access

ABAB
  • 25
  • 5
  • Did you try use `ADMIN` instead of `ROLE_ADMIN` – gurkan Apr 26 '23 at 15:15
  • I don't think that matters. it is all based on the roles I manually added to my MongoDB. It is used retrieved from the database { "_id": { "$oid": "6449278b7b0bcb5a0c8288c3" }, "userName": "Arnold Schwarzenegger", "email": "arnold@gmail.com", "password": "$2a$10$BYCGuOqfrogGRMESsNp7leH225A2vSmPzP3UoEPMMdBd8EV4uRB9S", "roles": [ "ROLE_SUPER_MANAGER", "ROLE_ADMIN", "ROLE_USER" ], "_class": "dev.barahow.movies.model.AppUser" } – ABAB Apr 26 '23 at 15:18
  • Turn on Spring security debug logging. That will give you information on why a user is denied access. `-Dlogging.level.org.springframework.security=DEBUG` – Christopher Schneider Apr 26 '23 at 15:20
  • Spring security adds "ROLE" prefix automatically. Please delete "ROLE" from them that are in DB and then try to use without prefix. https://stackoverflow.com/questions/33205236/spring-security-added-prefix-role-to-all-roles-name – gurkan Apr 26 '23 at 15:20
  • Unfortunately, deleting the role prefix didn't solve the issue. . – ABAB Apr 26 '23 at 15:40
  • 2
    Copy and paste your token in https://jwt.io As I can see from there, your token doesn't have any roles information in it. roles array is empty – Chetan Ahirrao Apr 26 '23 at 15:49
  • You are right, no clue why it is showing empty when I have roles information in my MongoDB – ABAB Apr 26 '23 at 15:53
  • Debug your code where you are fetching that info from MongoDB – Chetan Ahirrao Apr 26 '23 at 16:03
  • is there any specific reason to why you are not using the built in JWT functionality that already comes with the spring security framework? the whole point with using a security framework is to avoid needing to write a bunch of custom code – Toerktumlare Apr 26 '23 at 19:30
  • Just for learning purposes. I'm trying to implement multi-factor authentication and I have no clue how to do it with the built-in JWT functionality in java spring without creating a custom filter. – ABAB Apr 26 '23 at 20:13

1 Answers1

0

Solved

@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;

   // @JsonSerialize(using = ToStringSerializer.class)
    @DBRef
    private List<UserRole> roles = new ArrayList<>();



As someone pointed out my roles were not visible in my access token which made it difficult to authenticate a user. To solve the issue I added @DBRef above userRole instead of @DBReference. The issue was The @DBRef annotation indicates that the roles field is referencing another document in a separate collection, rather than embedding the UserRole objects within the document itself.

ABAB
  • 25
  • 5