spring-projects / spring-security

Spring Security
http://spring.io/projects/spring-security
Apache License 2.0
8.83k stars 5.9k forks source link

Support RFC 9457 error details for OAuth2 JWT validation #15549

Open bodote opened 3 months ago

bodote commented 3 months ago

Expected Behavior

when this is used:

 @Bean
    SecurityFilterChain configure(HttpSecurity http, AuthorizationManager<RequestAuthorizationContext> authz)
            throws Exception {
      http.oauth2ResourceServer(c -> c.jwt(Customizer.withDefaults()));

and a invalid JWT is used to access any Rest Endpoints, I would expect that spring framework returns RFC 9457 error details, or at least make it somehow configurable to do so.

Current Behavior

it returns an empty body and some 4xx error status.

Context

I tried some ideas using :

  SecurityFilterChain configure(HttpSecurity http, AuthorizationManager<RequestAuthorizationContext> authz)
            throws Exception {
        http.oauth2ResourceServer(oauth2 -> oauth2
                        .jwt(jwt -> jwt
                                .jwtAuthenticationConverter(jwtAuthenticationConverter())
                        )
                        .authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
                        .accessDeniedHandler(new BearerTokenAccessDeniedHandler())
                )
                .exceptionHandling(exceptionHandling -> exceptionHandling
                        .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
                );

and

@Slf4j
@RestControllerAdvice
public final class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
    @ExceptionHandler(JwtException.class)
    public ProblemDetail handleJwtException(JwtException ex) {
        ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.UNAUTHORIZED, ex.getMessage());
        problemDetail.setTitle("Unauthorized");
        problemDetail.setType(URI.create("https://example.com/unauthorized"));
        return problemDetail;
    }

but was never able to catch the JWTException in order to put RFC 9457 error details into the response body .

bodote commented 3 months ago

what I don't understand is why the HttpSecurity..oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()).authenticationEntryPoint(..) does not catch any Exception thrown by the JwtDecoder when the latter comes across an unvalid JWT.

The workaround I found so far, is to add a custom filter before the UsernamePasswordAuthenticationFilter http.addFilterBefore(new AuthenticationServiceExceptionFilter(), UsernamePasswordAuthenticationFilter.class) and catch the AuthenticationServiceException there. But is there no other way ? And what is the HttpSecurity..oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()).authenticationEntryPoint(..) good for anyway, if it seems not to catch any errors in JWT processing ?

BTW: using Spring Boot 3.2.5

ahmd-nabil commented 3 months ago

(I am not an expert and you should wait for an expert to answer more thoroughly)

  1. authenticationEntryPoint(..) does handle authorization errors by fetching error data and attaching them to WWW-Authenticate header complying with RFC 6750.
  2. @RestControllerAdvice doesn't catch spring security exception, that is because spring security filters happen before the request even reaches the dispatcher servlet (or controllers) more about that here spring-security-docs

if you need different behavior, my suggestion would be either keep your customExceptionHandlingFilter which is fine. or override AuthenticationEntryPoint.

     .authenticationEntryPoint(new YourCustomAuthenticationEntryPoint())
Toerktumlare commented 2 months ago

AuthenticationServiceException as the docs states is an exception that is thrown if your service takes the responsibility for not being able to process the request. For instance a sub service is not available, or a processing error has occurred.

AuthenticationEntryPoint is called in the ExceptionTranslationFilter when either a AccessDeniedException or AuthenticationException is thrown, or any subclass of them, or a wrapped such exception. This in turn semantically means that the service does not take responsibility and instead claims that the client has done something wrong security wise and will then for instance reroute them to a place where they can authenticate or elevate themselves. So basically the request was correct, but the client was not allowed to do what they asked for.

Providing a poorly formatted JWT isnt a security exception but more of a formatting/validation question, which means the client did something wrong and the exception should not be caught in the ExceptionTranslationFilter as the code current stands and as it does currently return a 4xx error telling the client that they did something wrong. Which they did.

devshrm commented 2 months ago

This fixed this issue for me : ~~ @patkovskyi, thanks for reaching out. Please instead specify the entry point directly to the authentication mechanism, like so:

http
    .authorizeRequests()
        // ...
    .oauth2ResourceServer()
        .authenticationEntryPoint(new CustomAuthenticationEntryPoint())
        .jwt()
        // ...

This will automatically configure exceptionHandling() to use your CustomAuthenticationEntryPoint as well.

Originally posted by @jzheaux in https://github.com/spring-projects/spring-security/issues/8961#issuecomment-693705813

~~