0
votes
  1. I use keycloak as a Central Authentication Service for (single sign on/out) feature.
  2. I have app1, app2, app3. app1 and app2 is monothetic application. app3 use spring session (use redis as session store),
  3. All feature work fine. But I use the back channel to logout for SSO(single sign out) feature, that's works for app1 and app2. But it not work for this app3.

I wonder how to back channel logout application that use spring session

1

1 Answers

0
votes

The keycloak admin url invoke when client user send a logout request to it.I find that KeycloakAutoConfiguration#getKeycloakContainerCustomizer() inject WebServerFactoryCustomizer for add KeycloakAuthenticatorValve, and that Valve use CatalinaUserSessionManagement, but it have not any info about redis as its session store. So I add a customizer for enhence the Valve.

  1. first i set the order of the autoconfig, because extra customizer must be callback after it.
@Slf4j
@Component
public class BeanFactoryOrderWrapper implements DestructionAwareBeanPostProcessor {
    @Override
    public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException {

    }

    @Override
    public boolean requiresDestruction(Object bean) {
        return true;
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if (beanName.equals("getKeycloakContainerCustomizer")) {
            Object wrapRes = this.wrapOrder(bean);
            return wrapRes;
        }
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    private Object wrapOrder(Object bean) {
        log.info("rewrite keycloak auto config customizer Order for next custom");
        final WebServerFactoryCustomizer origin = (WebServerFactoryCustomizer) bean;
        return new KeycloakContainerCustomizerWithOrder(origin);
    }
}

class KeycloakContainerCustomizerWithOrder implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>, Ordered {

    private final WebServerFactoryCustomizer origin;

    public KeycloakContainerCustomizerWithOrder(WebServerFactoryCustomizer origin) {
        this.origin = origin;
    }

    @Override
    public void customize(ConfigurableServletWebServerFactory factory) {
        origin.customize(factory);
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE - 1;
    }
}


  1. I extra RedisIndexedSessionRepository, and set it to proxy object
@Slf4j
@Configuration
@RequiredArgsConstructor
class ContainerConfig {
    private final RedisIndexedSessionRepository sessionRepository;

    @Bean
    public WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> getKeycloakContainerCustomizerGai() {
        return configurableServletWebServerFactory -> {
            if (configurableServletWebServerFactory instanceof TomcatServletWebServerFactory) {
                TomcatServletWebServerFactory container = (TomcatServletWebServerFactory) configurableServletWebServerFactory;
                container.getContextValves().stream().filter(ele -> ele.getClass() == KeycloakAuthenticatorValve.class).findFirst().map(ele -> (AbstractKeycloakAuthenticatorValve) ele).ifPresent(valve -> {
                    try {
                        final Field field = AbstractKeycloakAuthenticatorValve.class.getDeclaredField("userSessionManagement");
                        field.setAccessible(true);
                        final CatalinaUserSessionManagement origin = (CatalinaUserSessionManagement) field.get(valve);
                        field.set(valve, new CatalinaUserSessionManagementGai(origin, sessionRepository));
                    } catch (Exception e) {
                        log.error("enhence valve fail");
                    }
                });
            }
        };
    }
}

@Slf4j
class CatalinaUserSessionManagementGai extends CatalinaUserSessionManagement {
    private final CatalinaUserSessionManagement origin;
    private final RedisIndexedSessionRepository sessionRepository;

    public CatalinaUserSessionManagementGai(CatalinaUserSessionManagement origin, RedisIndexedSessionRepository sessionRepository) {
        this.origin = origin;
        this.sessionRepository = sessionRepository;
    }

    public void login(Session session) {
        origin.login(session);
    }

    public void logoutAll(Manager sessionManager) {
        origin.logoutAll(sessionManager);
    }

    public void logoutHttpSessions(Manager sessionManager, List<String> sessionIds) {
        for (String sessionId : sessionIds) {
            logoutSession(sessionManager, sessionId);
        }
    }

    protected void logoutSession(Manager manager, String httpSessionId) {
        try {
            final Method method = CatalinaUserSessionManagement.class.getDeclaredMethod("logoutSession", Manager.class, String.class);
            method.setAccessible(true);
            method.invoke(origin,manager,httpSessionId);
        } catch (Exception e) {
            log.error("session manager proxy invoke error");
        }

        // enhence part
        sessionRepository.deleteById(httpSessionId);
    }

    protected void logoutSession(Session session) {
        try {
            final Method method = CatalinaUserSessionManagement.class.getDeclaredMethod("logoutSession", Session.class);
            method.setAccessible(true);
            method.invoke(origin,session);
        } catch (Exception e) {
            log.error("session manager proxy invoke error");
        }
    }

    public void sessionEvent(SessionEvent event) {
        origin.sessionEvent(event);
    }
}

that work for me