2

In my Spring Boot (v. 2.7.0) app, I'm using Spring Security for authentication. If a user attempts to login with invalid credentials, the server responds with a 403 (Forbidden) status code, but I would expect 401 (Unauthorized) to be used instead.

I can't find anything in my configuration that indicates the default behaviour of Spring Security has been overriden, but whether it has or not, I want 401 to be returned when authentication fails.

I stepped through the relevant Spring Security code and it appears the Spring Security method SimpleUrlAuthenticationFailureHandler.onAuthenticationFailure is called when authentication fails. This method includes the line

response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());

But for some reason 403 is returned to the client - so I guess the response is being changed subsequent to the line above.

How can I change Spring Security to return 401 when authentication fails? I've included my security configuration below for reference.

@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true, jsr250Enabled = true)
public class SecurityConfiguration {

    @Autowired
    private AuthenticationConfiguration authenticationConfiguration;

    @Bean
    public SecurityFilterChain configure(HttpSecurity http) throws Exception {
        AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager();
        var jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager);

        var jwtAuthorisationFilter = new JwtAuthorisationFilter();

        http.cors().and().csrf().disable().authorizeRequests()
            .anyRequest().authenticated().and()
            .addFilter(jwtAuthenticationFilter)
            .addFilterAfter(jwtAuthorisationFilter, BasicAuthenticationFilter.class)
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        return http.build();
    }
}

The JwtAuthenticationFilter calls authenticationManager.authenticate which causes a org.springframework.security.authentication.BadCredentialsException to be thrown if the credentials are invalid.

Update

I tried adding a custom AuthenticationFailureHandler bean as described in this article, but my custom bean is never invoked (the default bean SimpleUrlAuthenticationFailureHandler is called instead).

Antonio Dragos
  • 1,973
  • 2
  • 29
  • 52
  • Have you read the full chapter on jwts in the spring security documentation? – Toerktumlare Jun 28 '22 at 17:17
  • @Toerktumlare not yet, no spoilers please. – Antonio Dragos Jun 28 '22 at 19:37
  • spoiler alert, JWTs are unsecure and were never ment to be handed out to browsers. Also, if someone steals your JWT in the browser you can't revoke it – Toerktumlare Jun 28 '22 at 19:42
  • @Toerktumlare that's not necessarily true. If you're persisting the JWTs, you could revoke it via the database and even if you're not, you could revoke them all by changing the key they're signed with. – Antonio Dragos Jun 28 '22 at 20:57
  • Revoking the key for all jwts is going to be very popular with customers surely. And great storing all jwts in a db, so i know which db i need to dump to get everyones jwts! – Toerktumlare Jun 28 '22 at 22:32
  • @Toerktumlare I'm not saying either of these is a perfect solution, but nonetheless your original statement that you can't revoke a JWT is incorrect – Antonio Dragos Jun 29 '22 at 09:15
  • No its not incorret. A plain single JWT can not be revoked. As you pointed you either need to revok ALL, or you need to store all JWTs in a database etc etc and implement all functionality that comes with cookie based sessions. So to tell you again, one single plain JWT cannot be revoked. JWT were mever meant to be handed out to browsers, they are less secure that cookie sessions since they are vuln to XSS – Toerktumlare Jun 29 '22 at 09:55

1 Answers1

0

You can provide your own custom AccessDeniedHandler implementation.

@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
    AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager();
    var jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager);

    var jwtAuthorisationFilter = new JwtAuthorisationFilter();

    http.cors().and().csrf().disable().authorizeRequests()
        .anyRequest().authenticated().and()
        .addFilter(jwtAuthenticationFilter)
        .addFilterAfter(jwtAuthorisationFilter, BasicAuthenticationFilter.class)
        .sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        .exceptionHandling()
        .accessDeniedHandler( (request, response, exception) ->
             response.sendError(HttpStatus.UNAUTHORIZED.value(), exception.getMessage()
        ));

    return http.build();
}
GnanaJeyam
  • 2,780
  • 16
  • 27
  • I'd just add that if an `AccessDeniedException` happens you should be returning 403 instead of 401, because it means that the user is authenticated but not authorized. You can configure Spring Security to return 401 on authentication failure by doing `exceptionHandling().authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));` – Marcus Hert da Coregio Jun 29 '22 at 13:56