Search code examples
spring-bootspring-securityoauth-2.0spring-authorization-server

Display warning that the redirect URI is invalid using the Spring Authorization server


I am using the Spring Authorization for OAuth2 and I want to display a warning to the user when the request to the /authorize endpoint has an invalid redirect uri i.e. uri that was not registered for the client.

Here is my Authorization server config:

@Configuration
public class SecurityConfiguration {

    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
            throws Exception {

        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                // Enable OpenID Connect 1.0
                .oidc(Customizer.withDefaults())
                .authorizationEndpoint(authorizationEndpoint ->
                        authorizationEndpoint
                                .errorResponseHandler(new CustomErrorResponseHandler()));

        http
                // Redirect to the login page when not authenticated from the
                // authorization endpoint
                .exceptionHandling((exceptions) -> exceptions
                        .defaultAuthenticationEntryPointFor(
                                new LoginUrlAuthenticationEntryPoint("/login"),
                                new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
                        )
                )
                // Accept access tokens for User Info and/or Client Registration
                .oauth2ResourceServer((resourceServer) -> resourceServer.jwt(Customizer.withDefaults()));

        return http.cors(Customizer.withDefaults()).build();
    }

    @Bean
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
            throws Exception {

        http
                .authorizeHttpRequests((authorize) -> authorize
                        .requestMatchers("/css/**").permitAll()
                        .requestMatchers("/").permitAll()
                        .anyRequest().authenticated()
                )
                // Form login handles the redirect to the login page from the
                // authorization server filter chain
                .formLogin(form -> form.loginPage("/login").permitAll())
                .logout(logout -> logout.logoutUrl("/logout"));

        return http.cors(Customizer.withDefaults()).build();
    }
}

and the CustomErrorResponseHandler is as follows

public class CustomErrorResponseHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        OAuth2AuthorizationCodeRequestAuthenticationException authorizationCodeRequestAuthenticationException =
                (OAuth2AuthorizationCodeRequestAuthenticationException) exception;
        OAuth2Error error = authorizationCodeRequestAuthenticationException.getError();
        OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
                authorizationCodeRequestAuthenticationException.getAuthorizationCodeRequestAuthentication();

        if (authorizationCodeRequestAuthentication == null ||
                !StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
            response.sendError(500);
        }
        else {
            response.sendError(HttpStatus.FORBIDDEN.value());
        }
    }
}

when I use Postman to test the authorization code flow I request the invalid scope on purpose. However instead of receiving the status code of 500 I am getting 302 redirect to /error. Why is the response.sendError(500) being ignored?

I have also tried to recover the status code in the error controller but it is always null.

@Controller
public class CustomErrorController implements ErrorController {

    @RequestMapping("/error")
    public String handleError(HttpServletRequest request) {
        Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);

        if (status != null) {
            int statusCode = Integer.parseInt(status.toString());

            System.out.println("Status code "+statusCode);
        }
        else {
            System.out.println("No status code");
        }
        return "error";
    }
}

Thanks a lot


Solution

  • To override the error behavior for the OAuth2 Authorization Endpoint, you can specify your own .errorResponseHandler(...) in the configurer, which should implement the AuthenticationFailureHandler interface. For example,

    @Bean
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
            new OAuth2AuthorizationServerConfigurer();
        http.apply(authorizationServerConfigurer);
    
        authorizationServerConfigurer
            .authorizationEndpoint(authorizationEndpoint ->
                authorizationEndpoint 
                    .errorResponseHandler((request, response, exception) -> {
                        // Handle errors with exception which is an instance of
                        // OAuth2AuthorizationCodeRequestAuthenticationException
                    }) 
            );
    
        return http.build();
    }
    

    See the sendErrorResponse method in OAuth2AuthorizationEndpointFilter for how it is handled by default.


    Update:

    If you intend to render an error (e.g. 400, 401, 403, 500, etc.) related to invalid scopes instead of performing a redirect back to the client, you need to permit the error page. By default, all endpoints including the error page require authentication.

    You can do so for example by adding .requestMatchers("/error").permitAll() to your authorization rules in the 2nd filter chain (defaultSecurityFilterChain).