0

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?

Askar
  • 544
  • 1
  • 6
  • 17

3 Answers3

0

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.

Askar
  • 544
  • 1
  • 6
  • 17
0

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();
}
0

The Keycloak adapters used in @Askar answer are deprecated and will probably never be compatible with boot 3. Don't use it. Alternative in the accepted answer to "Use Keycloak Spring Adapter with Spring Boot 3" (applicable to boot 2.6 and 2.7 with very little modification).

Here is sample of authentication exception customization in HttpSecurity configuration. You might do about anything with the response:

http.exceptionHandling().authenticationEntryPoint((request, response, authException) -> {
    response.addHeader(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"Restricted Content\"");
    response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
});
ch4mp
  • 6,622
  • 6
  • 29
  • 49
  • Good point, actually we are near to the end of current project. Certainly we will try `spring-security OAuth2` at the next project. – Askar Jan 25 '23 at 21:07