Home > front end >  How to customize 401 Unauthorized and 403 Forbidden HTTP responses when using Keycloak and Spring bo
How to customize 401 Unauthorized and 403 Forbidden HTTP responses when using Keycloak and Spring bo

Time:01-25

I get the following failure responses which the responses themselves are correct:

  • An HTTP 401 Unauthorized with empty body when the Keycloak token is invalid or expired.
  • An HTTP 403 Forbidden with stack trace in body when access is denied.

But how can one generate custom responses instead?

CodePudding user response:

I managed to customize responses in the following way:

A class with arbitrary name which implements AuthenticationFailureHandler and AccessDeniedHandler interfaces:

import com.fasterxml.jackson.databind.ObjectMapper;
import org.keycloak.adapters.springsecurity.authentication.KeycloakCookieBasedRedirect;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
// ... other required imports


@Component
public class CustomSecurityFailureResponseHandler implements AuthenticationFailureHandler, AccessDeniedHandler {

    private final ObjectMapper objectMapper = new ObjectMapper();
    
    // The body of this method has been copied from its Keycloak counterpart, except for the marked line.
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
        if (!response.isCommitted()) {
            if (KeycloakCookieBasedRedirect.getRedirectUrlFromCookie(request) != null) {
                response.addCookie(KeycloakCookieBasedRedirect.createCookieFromRedirectUrl(null));
            }
            writeResponse(response, HttpServletResponse.SC_UNAUTHORIZED, "Authentication failed");  // <--- marked line
        } else {
            if (200 <= response.getStatus() && response.getStatus() < 300) {
                throw new RuntimeException("Success response was committed while authentication failed!", exception);
            }
        }
    }

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        writeResponse(response, HttpServletResponse.SC_FORBIDDEN, "Access denied");
    }

    private void writeResponse(HttpServletResponse response, int status, String message) throws IOException {
        
        // Generate your intended response here, e.g.:
        
        response.setStatus(status);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");

        response.getOutputStream().println(
                objectMapper.writerWithDefaultPrettyPrinter()
                        .writeValueAsString(new MessageResponse(status, message)));
    }
    
    record ResponseMessage(int statusCode, String message) {}    
}

in the keycloak security config:

@KeycloakConfiguration
@EnableGlobalMethodSecurity(jsr250Enabled = true)
@Import(KeycloakSpringBootConfigResolver.class)
@RequiredArgsConstructor
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {

    // An object of preceding class is injected here
    private final CustomSecurityFailureResponseHandler customSecurityFailureResponseHandler;

    // Other required methods has been omitted for brevity
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http.csrf().disable()
            .authorizeRequests()
            .anyRequest().permitAll()
            .and()
            .exceptionHandling()
            .accessDeniedHandler(customSecurityFailureResponseHandler); // for 403 Forbidden
    }

    // This method is overridden to customize 401 Unauthorized response
    @Override
    public KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception {

        KeycloakAuthenticationProcessingFilter filter = super.keycloakAuthenticationProcessingFilter();
        filter.setAuthenticationFailureHandler(customSecurityFailureResponseHandler);

        return filter;
    }
}

The method keycloakAuthenticationProcessingFilter() is overridden to replace favorable AuthenticationFailureHandler.

The Keycloak 19.0.3 and the Spring boot 2.7.4 have been used.

I hope this workaround helps someone. Any better solution is appreciated.

CodePudding user response:

You can check the error code you receive back using something like this:

int error = response.getStatusLine().getStatusCode();

Then you can check the error if it triggers and display your own error:

if (error == 404)
{
    Toast.makeText(this, "custom message here", LENGTH_SHORT).show();
}
  • Related