Keycloak multi-tenacy: One realm's authentication is used to authenticate another realm

Viewed 40

I'm using keycloak-spring-security-adapter to secure my application and I have two keycloak realms.

  • The "Applications" realm is for single sign on and gets roles from Active Directory
  • The "S2S" realm is for system to system communication and gets roles from the "bearer" token in the Authentication header and has keycloak configuration bearer-only: true

I have two endpoints in my application

  • /api/client is secured by the "Applications" realm
  • /s2s/client is secured by the "S2S" realm

I'm witnessing the following behavior

  1. In the browser I hit /s2s/client and get a 401 unauthorized
  2. In the browser I hit /api/client and go through the keycloak redirect for SSO login and ultimately get a 200 response from the rest endpoint
  3. In the browser I hit /s2s/client and get a 200 response when I never provided a keycloak token for the S2S realm

I have the following config

security:
  keycloak:
    adapter-config:
      s2s:
        realm: S2S
        resource: MyClient
        auth-server-url: https://my-keycloak.com/auth
        ssl-required: none
        use-resource-role-mappings: true
        public-client: true
        bearer-only: true
      applications:
        realm: Applications
        resource: MyClient
        auth-server-url: https://my-keycloak.com/auth
        ssl-required: none
        use-resource-role-mappings: true
        public-client: true

And I have the following logic to pick a realm based on the URL which was inspired by the multi tenancy docs

import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.springsecurity.KeycloakConfiguration;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SecurityConfiguration {
    @Bean
    @ConfigurationProperties(prefix = "security.keycloak.adapter-config.s2s")
    public AdapterConfig s2sAdapterConfig() {
        return new AdapterConfig();
    }

    @Bean
    @ConfigurationProperties(prefix = "security.keycloak.adapter-config.applications")
    public AdapterConfig applicationsAdapterConfig() {
        return new AdapterConfig();
    }

    @Bean
    public KeycloakConfigResolver keycloakConfigResolver(
            @Qualifier("s2sAdapterConfig") AdapterConfig s2sAdapterConfig,
            @Qualifier("applicationsAdapterConfig") AdapterConfig applicationsAdapterConfig
    ) {
        KeycloakDeployment s2sDeployment = KeycloakDeploymentBuilder.build(s2sAdapterConfig);
        KeycloakDeployment applicationsDeployment = KeycloakDeploymentBuilder.build(applicationsAdapterConfig);
        return request -> request.getRelativePath().startsWith("/s2s/") ? s2sDeployment : applicationsDeployment;
    }
}   

I have the following @KeycloakConfiguration

@KeycloakConfiguration
public static class KeycloakSecurityConfiguration extends KeycloakWebSecurityConfigurerAdapter {
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(keycloakAuthenticationProvider());
    }

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

    @Bean
    protected SessionRegistry buildSessionRegistry() {
        return new SessionRegistryImpl();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http
            .authorizeRequests()
            .antMatchers("/api/**", "/s2s/**").authenticated()
            .antMatchers("/actuator/**", "/error").permitAll();
    }
}    

Version

18.0.2

Expected behavior

Requests to /s2s/* URL's should respond with 401 for all requests that do not have a valid token for the S2S realm

Actual behavior

Requests to /s2s/* URL's receive 200 OK responses when requested with a session cookie that has been authenticated for the Applications realm

How to Reproduce?

  1. Configure an "Applications" realm in a spring boot application
  2. Configure a "S2S" realm in the application with bearer-only: true
  3. Configure the KeycloakConfigResolver to pick "Applications" realm for \api\* URL's
  4. Configure the KeycloakConfigResolver to pick "S2S" realm for \s2s\* URL's
  5. Hit an \api\* URL in the browser and a 200 response (after following keycloak redirects for SSO / cookie)
  6. Hit an \s2s\* URL in the browser and get a 200 response (should be 401)
2 Answers

I raised issue #14301 on the keycloak github repository but didn't get a timely response

After doing some digging through the spring-security source code it turns out that the HttpSessionSecurityContextRepository was storing the authentication in the session which was then used for subsequent requests.

I hacked a fix with the following

import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.NullSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;

@KeycloakConfiguration
public static class KeycloakSecurityConfiguration extends KeycloakWebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // workaround for https://github.com/keycloak/keycloak/issues/14301
        SecurityContextRepository s2sRepository = new NullSecurityContextRepository();
        SecurityContextRepository otherRepository = new HttpSessionSecurityContextRepository();
        SecurityContextRepository repositoryDelegator = new SecurityContextRepository() {
            @Override
            public SecurityContext loadContext(HttpRequestResponseHolder requestResponse) {
                return getSecurityContextRepository(requestResponse.getRequest()).loadContext(requestResponse);
            }

            @Override
            public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
                getSecurityContextRepository(request).saveContext(context, request, response);
            }

            @Override
            public boolean containsContext(HttpServletRequest request) {
                return getSecurityContextRepository(request).containsContext(request);
            }

            private SecurityContextRepository getSecurityContextRepository(HttpServletRequest request) {
                return request.getServletPath().startsWith("/s2s/") ? s2sRepository : otherRepository;
            }
        };
        super.configure(http);
        http
            .securityContext().securityContextRepository(repositoryDelegator).and()
            .authorizeRequests()
            .antMatchers("/api/**", "/s2s/**").authenticated()
            .antMatchers("/actuator/**", "/error").permitAll();
    }
    ...
}

Do not use keycloak libs for spring it is deprecated (probable reason for not getting timely response). Read this article for alternatives.

With JWTs, you should go session-less (session-management set to stateless). This would solve your problem.

As a side note, spring-addons starter lib referenced at the end of the article does handle multi-tenancy.

Related