'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 ErrorPage
s 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 |