5

Description

I created an application which uses Keycloak 12.0.1 as identity provider. Single sign-on works fine, 'local logout' as well.

The Problem is the single sign-out.

I searched for documentations and issues in the web but found nothing much. I have three failing scenarios described by logs below.

Questions in the end are:

  • What am I doing wrong?
  • How must I implement the backchannel logout in my App?

Example how I understand SSOut should work:

  • User clicks 'logout' in App A
  • App A ends the session
  • App A notifies Keycloak
  • Keycloak notifies App B via backchannel logout
  • App B ends the session

Security configuration

The method keycloakCsrfRequestMatcher() frees library owned endpoints like "k_logout" from csrf protection but not my own url "/sso/logout". It might be possible to write my own Matchers but this is over my experience as developer.

import java.util.Arrays;
import java.util.List;

import javax.annotation.PostConstruct;

import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.KeycloakSecurityComponents;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.keycloak.adapters.springsecurity.filter.KeycloakSecurityContextRequestFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.env.Environment;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;

@Profile("KC")
@Configuration
@EnableWebSecurity
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
class SecurityConfigurationKeycloak extends KeycloakWebSecurityConfigurerAdapter implements EnvironmentAware {

    private static final Logger LOG = LoggerFactory.getLogger(SecurityConfigurationKeycloak.class);
    
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
        // SimpleAuthorityMapper is used to remove the ROLE_* conventions defined by
        // Java so we can use only admin or user instead of ROLE_ADMIN and ROLE_USER
        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
        auth.authenticationProvider(keycloakAuthenticationProvider);
    }

    @Bean
    public KeycloakSpringBootConfigResolver KeycloakConfigResolver() {
        return new KeycloakSpringBootConfigResolver();
    }

    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {


//      super.configure(http);

        http
            .csrf()
                .requireCsrfProtectionMatcher(keycloakCsrfRequestMatcher())
            .and()
                .sessionManagement()
                .sessionAuthenticationStrategy(sessionAuthenticationStrategy())
            .and()
                .addFilterBefore(keycloakPreAuthActionsFilter(), LogoutFilter.class)
                .addFilterBefore(keycloakAuthenticationProcessingFilter(), LogoutFilter.class)
                .addFilterAfter(keycloakSecurityContextRequestFilter(), SecurityContextHolderAwareRequestFilter.class)
                .addFilterAfter(keycloakAuthenticatedActionsRequestFilter(), KeycloakSecurityContextRequestFilter.class)
                .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint())
            /*
             * LOGOUT
             */
            .and()
                .logout()
                    .addLogoutHandler(keycloakLogoutHandler())
                    .logoutUrl("/sso/logout").permitAll()
                    .logoutSuccessUrl("/")

            .and()
                .authorizeRequests()

                /*
                 * ADMIN
                 */
                .antMatchers(
                        "/admin/**"
                    )
                .hasRole("ADMIN")

                /*
                 * PUBLIC
                 */
                .antMatchers(
                        "/webjars/**",
                        "/css/**",
                        "/img/**",
                        "/favicon.ico",
                        "/**")
                .permitAll();
    }

    @Override
    public void setEnvironment(Environment environment) {
        // TODO Auto-generated method stub
    }
}

Logs A

As we can see there is an CSRF error when KC tries to access the "/sso/logout" url. But I don't know if this is the right endpoint to use in KC? I found "/k_logout" in the used libraries, which seems to be some "internal redirect" url to me.

(Removed dates etc. for convenience.)

o.k.adapters.PreAuthActionsHandler       : adminRequest http://domain.tld/sso/logout
.k.a.t.AbstractAuthenticatedActionsValve : AuthenticatedActionsValve.invoke /sso/logout
o.k.a.AuthenticatedActionsHandler        : AuthenticatedActionsValve.invoke http://domain.tld/sso/logout
o.k.a.AuthenticatedActionsHandler        : Policy enforcement is disabled.
o.s.security.web.FilterChainProxy        : Securing POST /sso/logout
s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext
o.s.security.web.csrf.CsrfFilter         : Invalid CSRF token found for https://domain.tld/sso/logout
o.s.s.w.access.AccessDeniedHandlerImpl   : Responding with 403 status code
w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext
s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request
o.s.security.web.FilterChainProxy        : Securing POST /error
s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext
o.k.adapters.PreAuthActionsHandler       : adminRequest https://domain.tld/error
o.s.s.w.a.AnonymousAuthenticationFilter  : Set SecurityContextHolder to anonymous SecurityContext
o.s.s.w.a.i.FilterSecurityInterceptor    : Authorized filter invocation [POST /error] with attributes [permitAll]
o.s.security.web.FilterChainProxy        : Secured POST /error
e.p.p.controller.CustomErrorController   : User was not authorized for requested site: /error
w.c.HttpSessionSecurityContextRepository : Did not store anonymous SecurityContext
s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request

Logs B

If I use the "/k_logout" endpoint in KC instead I get an JWT Parse error in my App. I tried to debug it and it seems that in org.keycloak.jose.jws.JWSInput the encodedHeader is prefixed with "logout_token=" which seems to be the problem. At least to me. :-)

o.k.adapters.PreAuthActionsHandler       : adminRequest http://domain.tld/k_logout
o.k.adapters.PreAuthActionsHandler       : admin request failed, unable to verify token: Failed to parse JWT
o.k.adapters.PreAuthActionsHandler       : Failed to parse JWT

org.keycloak.common.VerificationException: Failed to parse JWT
    at org.keycloak.TokenVerifier.parse(TokenVerifier.java:402) ~[keycloak-core-12.0.1.jar:12.0.1]
    at org.keycloak.TokenVerifier.getHeader(TokenVerifier.java:423) ~[keycloak-core-12.0.1.jar:12.0.1]
    at org.keycloak.adapters.rotation.AdapterTokenVerifier.createVerifier(AdapterTokenVerifier.java:110) ~[keycloak-adapter-core-12.0.1.jar:12.0.1]
    at org.keycloak.adapters.PreAuthActionsHandler.verifyAdminRequest(PreAuthActionsHandler.java:210) ~[keycloak-adapter-core-12.0.1.jar:12.0.1]
    at org.keycloak.adapters.PreAuthActionsHandler.handleLogout(PreAuthActionsHandler.java:140) ~[keycloak-adapter-core-12.0.1.jar:12.0.1]
    at org.keycloak.adapters.PreAuthActionsHandler.handleRequest(PreAuthActionsHandler.java:80) ~[keycloak-adapter-core-12.0.1.jar:12.0.1]
    at org.keycloak.adapters.tomcat.AbstractKeycloakAuthenticatorValve.invoke(AbstractKeycloakAuthenticatorValve.java:177) ~[spring-boot-container-bundle-12.0.1.jar:12.0.1]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:888) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1597) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
    at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]
Caused by: org.keycloak.jose.jws.JWSInputException: com.fasterxml.jackson.core.JsonParseException: Unexpected character ((CTRL-CHAR, code 150)): expected a valid value (JSON String, Number, Array, Object or token 'null', 'true' or 'false')
 at [Source: (byte[])"��(���G��쉅����IL��؈�������耉)]P��������耉�
8�UL�ōYY����}=�!]�! �1]�}���1i���1]ጉ�"; line: 1, column: 2]
    at org.keycloak.jose.jws.JWSInput.<init>(JWSInput.java:58) ~[keycloak-core-12.0.1.jar:12.0.1]
    at org.keycloak.TokenVerifier.parse(TokenVerifier.java:400) ~[keycloak-core-12.0.1.jar:12.0.1]
    ... 19 common frames omitted
Caused by: com.fasterxml.jackson.core.JsonParseException: Unexpected character ((CTRL-CHAR, code 150)): expected a valid value (JSON String, Number, Array, Object or token 'null', 'true' or 'false')
 at [Source: (byte[])"��(���G��쉅����IL��؈�������耉)]P��������耉�
8�UL�ōYY����}=�!]�! �1]�}���1i���1]ጉ�"; line: 1, column: 2]
    at com.fasterxml.jackson.core.JsonParser._constructError(JsonParser.java:1851) ~[jackson-core-2.11.3.jar:2.11.3]
    at com.fasterxml.jackson.core.base.ParserMinimalBase._reportError(ParserMinimalBase.java:707) ~[jackson-core-2.11.3.jar:2.11.3]
    at com.fasterxml.jackson.core.base.ParserMinimalBase._reportUnexpectedChar(ParserMinimalBase.java:632) ~[jackson-core-2.11.3.jar:2.11.3]
    at com.fasterxml.jackson.core.json.UTF8StreamJsonParser._handleUnexpectedValue(UTF8StreamJsonParser.java:2686) ~[jackson-core-2.11.3.jar:2.11.3]
    at com.fasterxml.jackson.core.json.UTF8StreamJsonParser._nextTokenNotInObject(UTF8StreamJsonParser.java:865) ~[jackson-core-2.11.3.jar:2.11.3]
    at com.fasterxml.jackson.core.json.UTF8StreamJsonParser.nextToken(UTF8StreamJsonParser.java:757) ~[jackson-core-2.11.3.jar:2.11.3]
    at com.fasterxml.jackson.databind.ObjectMapper._initForReading(ObjectMapper.java:4664) ~[jackson-databind-2.11.3.jar:2.11.3]
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4513) ~[jackson-databind-2.11.3.jar:2.11.3]
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3529) ~[jackson-databind-2.11.3.jar:2.11.3]
    at org.keycloak.util.JsonSerialization.readValue(JsonSerialization.java:71) ~[keycloak-core-12.0.1.jar:12.0.1]
    at org.keycloak.jose.jws.JWSInput.<init>(JWSInput.java:56) ~[keycloak-core-12.0.1.jar:12.0.1]
    ... 20 common frames omitted

Logs C

If I disable csrf protection for the App completely by using .csrf().disable() the above error obviously is not there anymore. Instead the App can't map the logout request to a user.

o.k.adapters.PreAuthActionsHandler       : adminRequest http://192.168.178.31:8090/sso/logout
.k.a.t.AbstractAuthenticatedActionsValve : AuthenticatedActionsValve.invoke /sso/logout
o.k.a.AuthenticatedActionsHandler        : AuthenticatedActionsValve.invoke http://192.168.178.31:8090/sso/logout
o.k.a.AuthenticatedActionsHandler        : Policy enforcement is disabled.
o.s.security.web.FilterChainProxy        : Securing POST /sso/logout
s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext
o.k.adapters.PreAuthActionsHandler       : adminRequest http://192.168.178.31:8090/sso/logout
o.s.s.w.a.logout.LogoutFilter            : Logging out [null]
o.k.a.s.a.KeycloakLogoutHandler          : Cannot log out without authentication
o.s.s.web.DefaultRedirectStrategy        : Redirecting to /
w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext
s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request

Keycloak configuration for backchannel logout

Keycloak config

pom.xml

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>tld.domain</groupId>
    <artifactId>artifact</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>name</name>
    <description></description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.1</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>11</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

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

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
derBobby
  • 86
  • 1
  • 7
  • I'm using the Java Servlet Filter adapter and I'm having the exact same issue. Their documentation on this issue is incomplete, so it's hard to know exactly how to get this working. However, when I set my backchannel logout as `/keycloak/k_logout` my PreAuthActionsHandler seems to handle the request, however it errors out with the message you detailed in Logs B. Is this a legitimate bug in Keycloak? – Ring Feb 08 '21 at 23:33

0 Answers0