'Customize keycloak error page with spring boot

I use keycloak-spring-boot-starter to protect my rest-service from unauthorized access. The authentication works as expected, but if the authentication fails, then it returns an empty response.

However, I'd like to return a json error response similar to all my other error handlers. I already tried to define an @ExceptionHandler(Throwable.class), ErrorController, ErrorViewResolver or configuring the ErrorPages via WebServerCustomizer, but that doesn't work at all.

I'm totally fine, if I could define a static response for it.

There seems to be a property called delegateBearerErrorResponseSending, but I couldn't find where to set it. It isn't present in spring-boot's properties. I'm not even sure where the call will be delegated to.

There is a property called policy-enforcer-config.on-deny-redirect-to, but a redirect isn't the expected behavior for a rest service.

  • spring-boot: 2.3.1.RELEASE
  • keycloak-spring-boot-starter: 10.0.2

TLDR: How do I configure/customize the error page for keycloak.



Solution 1:[1]

I found a way to do this to a certain degree in current spring-security-web (5.4+) versions.

/**
 * A {@link RequestRejectedHandler} for spring security web's application firewall.
 */
@Component
public class FirewallRequestRejectedHandler implements RequestRejectedHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(FirewallRequestRejectedHandler.class);

    @Override
    public void handle(
            final HttpServletRequest request,
            final HttpServletResponse response,
            final RequestRejectedException requestRejectedException) throws IOException {

        // Optionally write a warning to the logs
        LOGGER.warn("Application firewall: {}", requestRejectedException.getMessage(),
                LOGGER.isDebugEnabled() ? requestRejectedException : null);

        // Make the exception accessible to the ErrorController
        request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, requestRejectedException );

        // Call error controller
        response.sendError(401, "Access denied");
    }

}

This will call the ErrorController which contains my fallback error handling logic, which results in the correct response being sent.

Solution 2:[2]

Hello friend?Overriding class method challengeResponse() be addressed

org.keycloak.adapters.BearerTokenRequestAuthenticator.challengeResponse( HttpFacade facade, final OIDCAuthenticationError.Reason reason, final String error, final String description )

This is how I enhanced keycloak-json-response-spring-boot-starter

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-json-response-spring-boot-starter</artifactId>
    <version>18.0.0</version>
    <name>keycloak-json-response-spring-boot-starter</name>
    <description>keycloak-json-response-spring-boot-starter</description>
    
    <!-- Omit some configuration ... -->

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.keycloak.bom</groupId>
                <artifactId>keycloak-adapter-bom</artifactId>
                <version>${keycloak.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-spring-boot-starter</artifactId>
        </dependency>
    </dependencies>

    <!-- Omit some configuration ... -->

Create the same package and class and then embed the code in the method

package org.keycloak.adapters;

@SuppressWarnings( "unused" )
public class BearerTokenRequestAuthenticator {

    protected AuthChallenge challengeResponse( HttpFacade facade, final OIDCAuthenticationError.Reason reason, final String error, final String description ) {
        StringBuilder header = new StringBuilder( "Bearer realm=\"" );
        header.append( deployment.getRealm( ) ).append( "\"" );

        // addon begin
        Map< String, String > responseBody = new LinkedHashMap<>( );
        HttpFacade.Response response = facade.getResponse( );
        // addon end

        // addon begin
        if ( reason == OIDCAuthenticationError.Reason.NO_BEARER_TOKEN ) {
            response.setStatus( 401 );
            responseBody.put( "error", "unauthorized" );
            responseBody.put( "error_description", "Not Token! To login." );
        }
        // addon end

        if ( error != null ) {
            header.append( ", error=\"" ).append( error ).append( "\"" );
            // addon begin
            responseBody.put( "error", error );
            response.setStatus( 403 );
            // addon end
        }
        if ( description != null ) {
            header.append( ", error_description=\"" ).append( description ).append( "\"" );
            // addon begin
            responseBody.put( "error_description", description );
            response.setStatus( 403 );
            // addon end
        }
        final String challenge = header.toString( );

        // addon begin
        try {
            if ( !responseBody.isEmpty( ) ) {
                response.setHeader( "Content-Type", "application/json" );
                OutputStream responseOutputStream = response.getOutputStream( );
                responseOutputStream.write( new ObjectMapper( ).writeValueAsBytes( responseBody ) );
                responseOutputStream.flush( );
            }
        } catch ( IOException e ) {
            e.printStackTrace( );
        }
        // addon end


        return new AuthChallenge( ) {
            @Override
            public int getResponseCode( ) {
                return 401;
            }

            @Override
            public boolean challenge( HttpFacade facade ) {
                if ( deployment.getPolicyEnforcer( ) != null ) {
                    deployment.getPolicyEnforcer( ).enforce( ( OIDCHttpFacade ) facade );
                    return true;
                }
                OIDCAuthenticationError error = new OIDCAuthenticationError( reason, description );
                facade.getRequest( ).setError( error );
                facade.getResponse( ).addHeader( "WWW-Authenticate", challenge );
                if ( deployment.isDelegateBearerErrorResponseSending( ) ) {
                    facade.getResponse( ).setStatus( 401 );
                } else {
                    facade.getResponse( ).sendError( 401 );
                }
                return true;
            }
        };
    }

}

Don't forget to copy the rest of the code in the class and Replace your keycloak-spring-boot-starter with keycloak-json-response-spring-boot-starter

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 ST-DDT
Solution 2