Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Boot keycloak and basic authentication together in the same project

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

like image 688
gubak Avatar asked Jun 21 '16 11:06

gubak


People also ask

Does Keycloak support basic authentication?

Yes that's possible for clients with Access Type: confidential and Direct Access Grants Enabled .

Which authentication is best in Spring Boot?

Which authentication is best in Spring boot? You can use custom token based implementation, you can create a custom token that you can store in DB but JWT is a good choice.


2 Answers

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());
    }
}
like image 102
Nejc Sever Avatar answered Sep 17 '22 14:09

Nejc Sever


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;
}
like image 32
kiskami Avatar answered Sep 16 '22 14:09

kiskami