Search code examples
spring-bootspring-securityspring-boot-security

Spring Boot Security custom authentication token is always null in the endpoint parameters


We have an issue receiving the authentication token in the parameters of our endpoint.

Below you can see a filter that creates an object of the Authorization class and adds it to the SecurityContext.

@Component
public class CustomFilter extends OncePerRequestFilter  {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        Authorization authorization = new Authorization(true, null);
        SecurityContextHolder.getContext().setAuthentication(authorization);

        filterChain.doFilter(request, response);
    }

}

This is what the Authorization class looks like.

@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
public class Authorization extends AbstractAuthenticationToken {

    private String token;

    private String subject;

    public Authorization(boolean authenticated, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        super.setAuthenticated(authenticated);
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return null;
    }
}

Below this is our current security configuration that we are testing our implementation on.

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain configure(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable) // Disable CSRF protection
                .sessionManagement(AbstractHttpConfigurer::disable) // Disable session management
                .authorizeHttpRequests(auth -> auth
                        .anyRequest()
                        .permitAll()
                )
                .addFilterBefore(new CustomFilter(), UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}

What we want to achieve is use @PreAuthorize to limit access and receive the Authorization object in the endpoint to use the JWT payload data. This should look like the endpoint below and there the PreAuthorize works just fine, even if we force them to be unauthenticated. The issue here is the Authorization parameter that is always giving null resulting in a 500 error code since it cannot call toString on null. So in short, the PreAuthorize works with the Authorization object, but receiving it in the parameters doesn't.

@GetMapping("/test")
@PreAuthorize("isAuthenticated()")
@ResponseStatus(HttpStatus.OK)
public TestingObject test(Authorization authorization) {
    Logger.console(ConsoleLogType.INFO, authorization.toString());
    return testService.test();
}

The weird part is that it does work if I change the Authorization object to a UsernamePasswordAuthenticationToken object. Then I can put either Authentication or UsernameAndPasswordAuthenticationToken as the parameter value and I actually receive the value in the endpoint.

@Component
public class CustomFilter extends OncePerRequestFilter  {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(null, null, null);
        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);

        filterChain.doFilter(request, response);
    }
}

// Both endpoints below work just fine
@GetMapping("/test")
@PreAuthorize("isAuthenticated()")
@ResponseStatus(HttpStatus.OK)
public TestingObject test(Authentication authentication) {
    Logger.console(ConsoleLogType.INFO, authentication.toString());
    return testService.test();
}

@GetMapping("/test")
@PreAuthorize("isAuthenticated()")
@ResponseStatus(HttpStatus.OK)
public TestingObject test(UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) {
    Logger.console(ConsoleLogType.INFO, usernamePasswordAuthenticationToken.toString());
    return testService.test();
}

I can also retrieve the Authorization object by calling SecurityContextHolder.getContext().getAuthentication(). See the example below.

@GetMapping("/test")
@PreAuthorize("isAuthenticated()")
@ResponseStatus(HttpStatus.OK)
public TestingObject test() {
    Authorization authorization = (Authorization) SecurityContextHolder.getContext().getAuthentication();
    Logger.console(ConsoleLogType.INFO, authorization.toString());
    return testService.test();
}

The issue is that we need to use the Authorization object for future tasks. We can access it using the SecurityContextHolder, but I prefer not use that. Especially since I am not sure what would happen if multiple people were to send a request at the same time, maybe it accesses someone elses authentication?

Of course we spend a ton of time researching and trying a ton of things. We tried too many things, so I can't go into detail on all of it. Below are some of the things we tried.

Our main idea is that Spring Boot Security has an allowance list. We saw that the AuthenticationProvider has a supports method which returns a boolean which seems like a way to define that it should be able to use the Authorization object. We tried implementing a custom AuthenticationProvider, but we can't seem to get it to use that provider. We tried specifying it in the security config, as an AuthenticationProvider bean, as part of the AuthenticationManager bean and even a combination of all 3.

Example of adding an AuthenticationProvider to the configuration.

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain configure(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                        .anyRequest()
                        .permitAll()
                )
                .authenticationProvider(new AuthenticationProvider() {
                    @Override
                    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
                        System.out.println("print"); // This never prints
                        return authentication;
                    }

                    @Override
                    public boolean supports(Class<?> authentication) {
                        System.out.println("print"); // This never prints
                        return true;
                    }
                })
                .addFilterBefore(new CustomFilter(), UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}

We also tried defining a bean of AuthenticationManager where we specify the AuthenticationProvider. However, I have lost this code.

We tried creating a new SecurityContext to work with inside the filter.

SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(authorization);
SecurityContextHolder.setContext(securityContext);

Using the @AuthenticationPrinciple annotation in the parameters.

@GetMapping("/test")
@PreAuthorize("isAuthenticated()")
@ResponseStatus(HttpStatus.OK)
public TestingObject test(@AuthenticationPrincipal Authorization authorization) {
    Logger.console(ConsoleLogType.INFO, authorization.toString());
    return testService.test();
}

Does anyone know why we can't just receive Authorization in our endpoint parameters?


Solution

  • I imagine it's due to you returning null from getPrincipal().

    There are a couple of steps here to understand why; however, the main principle to know is that Spring Security expects there to be some kind of principal, even if that principal is the bearer of the authorization token.

    First, Spring MVC publishes an argument resolver that injects the value of HttpServletRequest#getUserPrincipal into method parameters that implement java.security.Principal (which Authorization does).

    Second, Spring Security wraps #getUserPrincipal like so:

    @Override
    public Principal getUserPrincipal() {
        Authentication auth = getAuthentication();
        if ((auth == null) || (auth.getPrincipal() == null)) {
            return null;
        }
        return auth;
    }
    

    So, because Authorization#getPrincipal returns null, HttpServletRequest#getUserPrincipal does too, resulting in the method parameter being null.