Spring Security Keycloak Adapter cannot handle single sign-out when logout from another tab in browser

I have implemented a spring boot web app using spring security and Keycloak to authenticate users. I used maven, spring boot 2.2.2 and Keycloak 8.0.1. All things works correctly except a problem in single sign-out. When I open the secured path of spring boot app in a tab of my browser (http://localhost:8080/books) and Keycloak account page (http://localhost:8180/auth/realms/{realm_name}/account) in another tab and login with one of the users in one of them, the other tab will aware of the login and after reload the page, that page will also be authenticated. But the problem comes in when both tabs are logged in and first I log out from account page, and reload spring boot app, the user remains active and the app does not aware of the log out action in other tabs. I also checked active sessions of the user in keycloak admin console and there is no active session after logout, but the spring boot app remains showing protected endpoint. How can I handle this problem?

my project resource tree is like image below:

This is my 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.sso</groupId>
    <artifactId>demoapp</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demoapp</name>
    <description>Demo project for Spring Boot</description>

    <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>13</java.version>
        <keycloak.version>8.0.1</keycloak.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</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-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>

        <!-- Keycloak Adapter -->
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-test-helper</artifactId>
            <version>${keycloak.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <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>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

This is SecurityConfig.java :

@KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) {
        SimpleAuthorityMapper grantedAuthorityMapper = new SimpleAuthorityMapper();
        grantedAuthorityMapper.setPrefix("ROLE_");

        KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(grantedAuthorityMapper);
        auth.authenticationProvider(keycloakAuthenticationProvider);
    }

    /**
     * Defines the session authentication strategy.
     */
    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
    }

    /**
     * Define an HttpSessionManager bean only if missing.
     */
    @Bean
    @Override
    @ConditionalOnMissingBean(HttpSessionManager.class)
    protected HttpSessionManager httpSessionManager() {
        return new HttpSessionManager();
    }

    /**
     * Define security constraints for the application resources.
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http
            .authorizeRequests()
            .antMatchers("/books").authenticated()
            .antMatchers("/manager").hasRole("admin")
            .anyRequest().permitAll();
    }

    @Bean
    public ServletListenerRegistrationBean<HttpSessionEventPublisher> httpSessionEventPublisher() {
        return new ServletListenerRegistrationBean<HttpSessionEventPublisher>(new HttpSessionEventPublisher());
    }
}

This is LibraryController.java :

@Controller
public class LibraryController {
    private final HttpServletRequest request;
    private final BookRepository bookRepository;

    @Autowired
    public LibraryController(HttpServletRequest request, BookRepository bookRepository) {
        this.request = request;
        this.bookRepository = bookRepository;
    }

    @GetMapping(value = "/")
    public String getHome() {
        return "index";
    }

    @GetMapping(value = "/books")
    public String getBooks(Model model) {
        configCommonAttributes(model);
        model.addAttribute("books", bookRepository.readAll());
        return "books";
    }

    @GetMapping(value = "/manager")
    public String getManager(Model model) {
        configCommonAttributes(model);
        model.addAttribute("books", bookRepository.readAll());
        return "manager";
    }

    @GetMapping(value = "/logout")
    public String logout() throws ServletException {
        request.logout();
        return "redirect:/";
    }

    private void configCommonAttributes(Model model) {
        model.addAttribute("firstname", getKeycloakSecurityContext().getIdToken().getGivenName());
        model.addAttribute("lastname", getKeycloakSecurityContext().getIdToken().getFamilyName());
        model.addAttribute("email", getKeycloakSecurityContext().getIdToken().getEmail());
    }

    /**
     * The KeycloakSecurityContext provides access to several pieces of information
     * contained in the security token, such as user profile information.
     */
    private KeycloakSecurityContext getKeycloakSecurityContext() {
        return (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
    }
}

and below is my DemoappApplication.java :

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoappApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoappApplication.class, args);
    }
}

Have you tried to clean cookies?
Also you could try to use incognito mode. It’s not a solution of course, but a kind of workaround
Or different browsers

Hi,

I also wonder if you have “Admin URL” configured in the admin console
for the OIDC client, which corresponds to your Spring Boot application?
As when you logout from the account console, the logout is propagated to
Keycloak server. Keycloak server is able to propagate this “logout”
request to server-side applications, but it requires “Admin URL” configured.

Also, I suggest to take a look at some Keycloak examples and quickstarts
related to Spring Boot and try the scenario there and see if it works
and how the “Admin URL” needs to be configured in case of Spring Boot
applications.

Marek

2 Likes

Yes. I have used HttpServletRequest.logout() in my controller class which is the way mentioned in Keycloak official documentation.

Hi,
Yes, It worked. As you said the problem is just solved by setting the base URL of my spring boot application as Admin-URL in the corresponding client configuration page of Keycloak server. Now, the spring boot app is aware of user log out from other apps. Many thanks for your help.

1 Like

I tried too, but it is not getting logged out from client 2…

client 1(react), client 2(ftl page in spring boot)
single sign on works well for both apps, but when i try to logout from the client 1, im not logged out from the client 2 but the sessions in the idp are removed.

I haven’t read all the thread, but based on the last question, you want to enable SLO feature. Just check OIDC backchannel Logout [1] (I’m assuming that both apps have a backend), you will find here [2] / [3] how to enable it in KC.

[1] Final: OpenID Connect Back-Channel Logout 1.0 incorporating errata set 1
[2] Server Administration Guide
[3] Server Administration Guide