22

I have an issue with Spring Boot security. What I want is to have two different authentication for the same project at the same time in Spring Boot. The one is SSO (keycloak authentication) for all path except '/download/export/*' , the other one is Spring Boot basic authentication. Here is my configuration file:

@Configuration 
@EnableWebSecurityp 
public class MultiHttpSecurityConfig {
@Configuration
@Order(1)
public static class DownloadableExportFilesSecurityConfig extends WebSecurityConfigurerAdapter
{
@Override
protected void configure(HttpSecurity http) throws Exception
{
    http
            .antMatcher("/download/export/test")
            .authorizeRequests()
            .anyRequest().hasRole("USER1")
            .and()
            .httpBasic();    }

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception
{
    auth.inMemoryAuthentication()
            .withUser("user").password("password1").roles("USER1");
}
}

@Configuration
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
public static class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter
{
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception
{
    auth.authenticationProvider(keycloakAuthenticationProvider());
}

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

@Override
protected void configure(HttpSecurity http) throws Exception
{
    super.configure(http);
    http
            .regexMatcher("^(?!.*/download/export/test)")
            .authorizeRequests()
            .anyRequest().hasAnyRole("ADMIN", "SUPER_ADMIN")
            .and()
            .logout().logoutSuccessUrl("/bye");

}
}

The problem with above code is the following: If I request url '/download/export/test', than it asks me the username/password (Basic authentication). After successful login it asks me again for username/password (but this time keycloak authentication) , even if the requested url is excluded from SecurityConfig (Keycloak Adapter).

It gives me only a warning:

2016-06-20 16:31:28.771  WARN 6872 --- [nio-8087-exec-6] o.k.a.s.token.SpringSecurityTokenStore   : Expected a KeycloakAuthenticationToken, but found org.springframework.security.authentication.UsernamePasswordAuthenticationToken@3fb541cc: Principal: org.springframework.security.core.userdetails.User@36ebcb: Username: user; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER1; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@957e: RemoteIpAddress: 127.0.0.1; SessionId: 4C1BD3EA1FD7F50477548DEC4B5B5162; Granted Authorities: ROLE_USER1

Do you have any ideas how to use keycloak and basic authentication together?

Many many thanks! Carlo

gubak
  • 343
  • 1
  • 4
  • 9

2 Answers2

32

Problem explanation

The problem you're having is that KeycloakAuthenticationProcessingFilter.java intercepts every request with HTTP Authorization header. If your request is not authenticated with Keycloak (Even if you're authenticated with any other authentication provider! - in your case with basic authentication) you'll always either be redirected to Keycloak's login page (in your case) or get 401 Unauthorized (if your Keycloak client in keycloak.json is configured to bearer-only).

By default KeycloakAuthenticationProcessingFilter.java is invoked if request matches KeycloakAuthenticationProcessingFilter.DEFAULT_REQUEST_MATCHER:

public static final RequestMatcher DEFAULT_REQUEST_MATCHER =
    new OrRequestMatcher(
            new AntPathRequestMatcher(DEFAULT_LOGIN_URL),
            new RequestHeaderRequestMatcher(AUTHORIZATION_HEADER),
            new QueryParamPresenceRequestMatcher(OAuth2Constants.ACCESS_TOKEN)
    );

This means that any request that matches DEFAULT_LOGIN_URL (/sso/login) OR contains Authorization HTTP header (in your case) OR has access_token as query parameter, will be processed by KeycloakAuthenticationProcessingFilter.java.

That's why you have to replace RequestHeaderRequestMatcher(AUTHORIZATION_HEADER) with your own implementation that will skip invocation of KeycloakAuthenticationProcessingFilter.java when request is authenticated with basic authentication.

Solution

Below is a full solution that enables you to use both Basic authentication and Keycloak authentication simultaneously on the same paths. Pay special attention to IgnoreKeycloakProcessingFilterRequestMatcher implementation which is replacing default RequestHeaderRequestMatcher. This matcher will match only requests containing Authorization HTTP header which value is not prefixed with "Basic ".

In example below, user with role TESTER can access /download/export/test while all other paths are available to users with ADMIN or SUPER_ADMIN roles (which I assume, in your case, are accounts on Keycloak server).

@KeycloakConfiguration
public class MultiHttpSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("tester")
                .password("testerPassword")
                .roles("TESTER");
        auth.authenticationProvider(keycloakAuthenticationProvider());
    }

    @Bean
    @Override
    protected KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception {
        RequestMatcher requestMatcher =
                new OrRequestMatcher(
                        new AntPathRequestMatcher(DEFAULT_LOGIN_URL),
                        new QueryParamPresenceRequestMatcher(OAuth2Constants.ACCESS_TOKEN),
                        // We're providing our own authorization header matcher
                        new IgnoreKeycloakProcessingFilterRequestMatcher()
                );
        return new KeycloakAuthenticationProcessingFilter(authenticationManagerBean(), requestMatcher);
    }

    // Matches request with Authorization header which value doesn't start with "Basic " prefix
    private class IgnoreKeycloakProcessingFilterRequestMatcher implements RequestMatcher {
        IgnoreKeycloakProcessingFilterRequestMatcher() {
        }

        public boolean matches(HttpServletRequest request) {
            String authorizationHeaderValue = request.getHeader("Authorization");
            return authorizationHeaderValue != null && !authorizationHeaderValue.startsWith("Basic ");
        }
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http.authorizeRequests()
                .antMatchers("/download/export/test")
                .hasRole("TESTER")
                .anyRequest()
                .hasAnyRole("ADMIN", "SUPER_ADMIN")
                .and()
                .httpBasic();
    }

    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
    }
}
Nejc Sever
  • 872
  • 8
  • 11
  • Sorry but which version of keycloak-spring-security-adapter are you using ? – TheBakker Jun 11 '18 at 15:32
  • This example is for version 3.4.3.Final. – Nejc Sever Jun 12 '18 at 19:32
  • With spring-boot 2.3.0, I get this error, The bean 'httpSessionManager', defined in class path resource [net/ifao/companion/ccbd/config/KeycloakSecurityConfiguration.class], could not be registered. A bean with that name has already been defined in URL [jar:file:/C:/Users/wjose/.m2/repository/org/keycloak/keycloak-spring-security-adapter/5.0.0/keycloak-spring-security-adapter-5.0.0.jar!/org/keycloak/adapters/springsecurity/management/HttpSessionManager.class] and overriding is disabled. – Winster Jun 05 '20 at 15:34
  • use the matching version of keycloak if you are using spring-boot 2.3.0. Then u need to use 6.0.1 onwards https://mvnrepository.com/artifact/org.keycloak/keycloak-spring-boot-starter/6.0.1 – Puneeth Rai Sep 06 '20 at 16:53
  • 2
    Great solution thanks :) I have an optimisation for it which is more future version compatible: RequestMatcher requestMatcher = new AndRequestMatcher(KeycloakAuthenticationProcessingFilter.DEFAULT_REQUEST_MATCHER, // We're providing our own authorization header matcher new IgnoreKeycloakProcessingForBasicAuthenticationFilterRequestMatcher() ); – Selim Ok May 14 '21 at 23:26
  • This doesn't work for me, unless I disable the enforcer for that path but then it doesn't display Spring's login page for basic auth. I have this settings: `http. authenticationProvider(keycloakAuthenticationProvider()) .authorizeRequests() .antMatchers("/**").permitAll() .antMatchers(oAuth2AuthenticationSettings.getActuatorNonProtectedUrlPattern()).permitAll() .antMatchers(oAuth2AuthenticationSettings.getActuatorPrefix() + "/**").hasRole("ACTUATOR_USER") .and().httpBasic();` – xbmono Jul 13 '21 at 09:49
  • can you please check my settings (previous comment) and see if I made any mistake? – xbmono Jul 13 '21 at 09:51
2

I solved this by configuring an exception on KeycloakAuthenticationProcessingFilter for the path:

...
@Configuration
@Order(2)
static class KeyCloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {

@Bean
public KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception {
    KeycloakAuthenticationProcessingFilter filter = new KeycloakAuthenticationProcessingFilter(
            authenticationManagerBean()
            , new AndRequestMatcher(
               KeycloakAuthenticationProcessingFilter.DEFAULT_REQUEST_MATCHER,
               new NegatedRequestMatcher(new AntPathRequestMatcher(YOUR_BASIC_AUTHD_PATH))));
    filter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy());
    return filter;
}
kiskami
  • 21
  • 3