I’ve noticed some issues when testing single logout with the spring security adapter.
I setup the admin url for the test application that used the spring security adapter in keycloak and tested logging out from keycloak and it didn’t invalidate the session. This is consistent with what I saw in other environments while
testing. I did some digging and found that the spring adapter isn’t working correctly for single log out in my environments. We’re not using spring boot so not sure if that might be a reason why its not working out of the box.
The issue is with the org.keycloak.adapters.springsecurity.management.HtttpSessionManager class. This implements javax.servlet.http.HttpSessionListener to receive events when sessions are created and stores the sessions in a hash map.
When you do a logout from keycloak, it sends a POST request to <admin_url>/k_logout. This results in a call to the HttpSessionManager.logoutHttpSessions method with the session id passed in as an argument. This method attempts to lookup the session in the
hashmap and call the invalidate() method.
The problem is by default the HttpSessionManager class isn’t receiving the session create events. You need to configure it as a listener in web.xml to enable that. But even if you do that it still doesn’t work because the servlet container
will create a instance of the class, but spring will also create another instance when creating the keycloak beans and this new instance is the one passed into the KeycloakPreAuthActionsFilter constructor. So the instance that is created by the servlet container
is the one receiving the session create event and the one used by spring isn’t receiving any events but is the one used to do the logoutHttpSessions() call. The spring instance has no sessions in the hashmap, so logoutHttpSessions() does nothing.
The fix is to make a new version of HttpSessionManager that implements org.keycloak.adapters.spi.UserSessionManagement
and org.springframework.context.ApplicationListener<ApplicationEvent>, which is a spring interface that receives session create/destroy events. In web.xml you need to register org.springframework.security.web.session.HttpSessionEventPublisher as a listener
so spring will receive those events from the servlet container. Then in the spring config, you need the KeycloakPreAuthActionsFilter to be initialized with the new HttpSessionManager instead of the default one.
The HttpSessionManager class that works for me is below…
package my.keycloak; import java.util.List; import javax.servlet.http.HttpSession; import org.keycloak.adapters.spi.UserSessionManagement; import org.keycloak.adapters.springsecurity.management.LocalSessionManagementStrategy; import org.keycloak.adapters.springsecurity.management.SessionManagementStrategy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.security.web.session.HttpSessionCreatedEvent; import org.springframework.security.web.session.HttpSessionDestroyedEvent; public
class HttpSessionManager
implements UserSessionManagement, ApplicationListener<ApplicationEvent> {
private
static
final Logger
log = LoggerFactory.getLogger(HttpSessionManager.class);
private SessionManagementStrategy
sessions =
new LocalSessionManagementStrategy();
@Override
public
void logoutAll() {
log.info("Received request to
log out all users.");
for (HttpSession
session :
sessions.getAll()) {
session.invalidate(); }
sessions.clear(); }
@Override
public
void logoutHttpSessions(List<String>
ids) {
log.info("Received request to
log out {} session(s): {}",
ids.size(),
ids);
for (String
id :
ids) { HttpSession
session =
sessions.remove(id);
if (session
!= null) {
session.invalidate(); } }
sessions.clear(); }
@Override
public
void onApplicationEvent(ApplicationEvent
event) {
if (event
instanceof HttpSessionCreatedEvent) { HttpSessionCreatedEvent
e = (HttpSessionCreatedEvent)event; HttpSession
session =
e.getSession();
log.debug("Session created: {}",
session.getId());
sessions.store(session); }
else
if (event
instanceof HttpSessionDestroyedEvent) { HttpSessionDestroyedEvent
e = (HttpSessionDestroyedEvent)event; HttpSession
session =
e.getSession();
sessions.remove(session.getId());
log.debug("Session destroyed:
{}",
session.getId());
}
} } |
The keycloak config changes are below…
@Configuration @EnableWebSecurity @ComponentScan(basePackageClasses = KeycloakSecurityComponents.class) public
class WebSecurityConfig
extends KeycloakWebSecurityConfigurerAdapter {
@Autowired public
void configureGlobal(AuthenticationManagerBuilder
auth)
throws Exception {
auth.authenticationProvider(keycloakAuthenticationProvider()); }
@Override protected SessionAuthenticationStrategy sessionAuthenticationStrategy()
{
return
new RegisterSessionAuthenticationStrategy(new
SessionRegistryImpl()); }
@Bean
protected KeycloakPreAuthActionsFilter keycloakPreAuthActionsFilter() {
return
new KeycloakPreAuthActionsFilter(springHttpSessionManager()); }
@Bean
protected my.keycloak.HttpSessionManager
springHttpSessionManager() {
return
new my.keycloak.HttpSessionManager(); }
@Override protected
void configure(HttpSecurity
http)
throws Exception { super.configure(http);
http .logout() .logoutRequestMatcher(new
AntPathRequestMatcher("/sso/logout")) .and() .authorizeRequests() .antMatchers("/user*").authenticated() .anyRequest().permitAll(); } } |
and web.xml needs this added to it…
<listener>
<listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
</listener> |
After making the above changes, log out from the keycloak admin console works as expected.
Regards,
Anthony Fryer