0

We have been using Keycloak with Spring Security (Spring Boot 2) for a while and now we're trying to add a custom API-Key authentication mechanism where we check for a header called api-key and send that value to a remote service for verification, and if it valid, skip the Keycloak check entirely. This applies to all requests and endpoints.

I have my own AuthenticationProvider and AbstractAuthenticationProcessingFilter, but now all requests to the server throw a 403, even valid Keycloak requests. Strangely, none of my new code is even being executed as proven by no signs of logging or breakpoint hits. I have read through the multiple authentication documentation and reviewed several SO posts, yet still can't get it working.

Here is my custom AuthenticationProvider:

public class ApiKeyAuthenticationProvider implements AuthenticationProvider {

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        log.info("API-KEY: Provider.authenticate()");

        ApiKeyAuthenticationToken auth = (ApiKeyAuthenticationToken) authentication;

        String apiKey = auth.getCredentials().toString();

        // Always returns TRUE at the moment to test bypassing Keycloak
        boolean isApiKeyValid = RemoteApiKeyService.verify(apiKey);

        if (isApiKeyValid) {
            log.info("API-KEY: auth successful");
            auth.setAuthenticated(true);
        } else {
            log.warn("API-KEY: auth failed");
            throw new BadCredentialsException("Api-Key Authentication Failed");
        }

        return auth;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        log.info("API-KEY: Provider.supports(): " + authentication.getSimpleName());
        return authentication.isAssignableFrom(ApiKeyAuthenticationToken.class);
    }
}

My Token:

public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {

    private final String token;

    public ApiKeyAuthenticationToken(String token) {
        super(null);
        this.token = token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }

    @Override
    public Object getPrincipal() {
        return null;
    }
}

Here is the Filter:

public class ApiKeyFilter extends AbstractAuthenticationProcessingFilter {

    public ApiKeyFilter() {
        super("/*");
        log.info("API-KEY filter.init()");
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        log.info("API-KEY filter.attemptAuthentication()");
        String apiKeyHeader = request.getHeader("api-key");
        if (apiKeyHeader != null) {
            return new ApiKeyAuthenticationToken(apiKeyHeader);
        }

        return null;
    }
}

And finally, how I'm tying everything together with my security configuration using multiple providers:

@Slf4j
@Configuration
@EnableWebSecurity
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
public class SecurityConf {

  @Configuration
  @Order(1) //Order is 1 -> First the special case
  public static class ApiKeySecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception
    {
      http.csrf().disable().authorizeRequests()
              .antMatchers("/**").authenticated();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
      // our custom authentication provider
      auth.authenticationProvider(new ApiKeyAuthenticationProvider());
    }
  }

  @Configuration
  @Order(2) // processed after our API Key bean config
  public static class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
      KeycloakAuthenticationProvider provider = keycloakAuthenticationProvider();
      provider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
      auth.authenticationProvider(provider);
    }

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
      super.configure(http);

      http.csrf().disable().authorizeRequests();
      http.headers().frameOptions().disable();
    }

    // necessary due to http://www.keycloak.org/docs/latest/securing_apps/index.html#avoid-double-filter-bean-registration
    @Bean
    public FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean(KeycloakAuthenticationProcessingFilter filter) {
      FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
      registrationBean.setEnabled(false);
      return registrationBean;
    }

    // necessary due to http://www.keycloak.org/docs/latest/securing_apps/index.html#avoid-double-filter-bean-registration
    @Bean
    public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean(KeycloakPreAuthActionsFilter filter) {
      FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
      registrationBean.setEnabled(false);
      return registrationBean;
    }

    // necessary due to http://www.keycloak.org/docs/latest/securing_apps/index.html#avoid-double-filter-bean-registration
    @Bean
    public FilterRegistrationBean keycloakAuthenticatedActionsFilterBean(
            KeycloakAuthenticatedActionsFilter filter) {
      FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
      registrationBean.setEnabled(false);
      return registrationBean;
    }

    // necessary due to http://www.keycloak.org/docs/latest/securing_apps/index.html#avoid-double-filter-bean-registration
    @Bean
    public FilterRegistrationBean keycloakSecurityContextRequestFilterBean(
            KeycloakSecurityContextRequestFilter filter) {
      FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
      registrationBean.setEnabled(false);
      return registrationBean;
    }


    @Bean
    @Scope(value = "singleton")
    public KeycloakSpringBootConfigResolver keycloakConfigResolver() {

      final KeycloakDeployment keycloakDeployment = KeycloakDeploymentBuilder.build(
              KeycloakClient.default_client.toAdapterConfig()
      );

      return new KeycloakSpringBootConfigResolver() {

        @Override
        public KeycloakDeployment resolve(HttpFacade.Request request) {
          return keycloakDeployment;
        }

      };
    }
  }
}

Any idea what's been misconfigured? The fact that none of my code even runs yet breaks Keycloak is interesting.

J-Deq87
  • 101
  • 10

1 Answers1

0

Take this as an example and try it accordingly in your code

@Override
    protected void configure(HttpSecurity http) throws Exception {
        AuthenticationProvider rememberMeAuthenticationProvider = rememberMeAuthenticationProvider();
        TokenBasedRememberMeServices tokenBasedRememberMeServices = tokenBasedRememberMeServices();

        List<AuthenticationProvider> authenticationProviders = new ArrayList<AuthenticationProvider>(2);
        authenticationProviders.add(rememberMeAuthenticationProvider);
        authenticationProviders.add(customAuthenticationProvider);
        AuthenticationManager authenticationManager = authenticationManager(authenticationProviders);

        http
                .csrf().disable()
                .headers().disable()
                .addFilter(new RememberMeAuthenticationFilter(authenticationManager, tokenBasedRememberMeServices))
                .rememberMe().rememberMeServices(tokenBasedRememberMeServices)
                .and()
                .authorizeRequests()
                .antMatchers("/js/**", "/css/**", "/img/**", "/login", "/processLogin").permitAll()
                .antMatchers("/index.jsp", "/index.html", "/index").hasRole("USER")
                .antMatchers("/admin", "/admin.html", "/admin.jsp", "/js/saic/jswe/admin/**").hasRole("ADMIN")
                .and()
                .formLogin().loginProcessingUrl("/processLogin").loginPage("/login").usernameParameter("username").passwordParameter("password").permitAll()
                .and()
                .exceptionHandling().accessDeniedPage("/login")
                .and()
                .logout().permitAll();
    }

NOTE: The key point here is to add token and filter in configuration file as explained above. sorry for not posting the direct answer, since i had this, and it will give you a wide area to work on or an idea to go an work on to make the code run properly

  • Is this `configure()` supposed to be in my first class (bean `@Order(1)`) that extends `WebSecurityConfigurerAdapter` or am I customizing the second class that extends `KeycloakWebSecurityConfigureAdapter`? According to the official [documentation](https://docs.spring.io/spring-security/site/docs/4.2.x/reference/htmlsingle/#multiple-httpsecurity) for multiple auth providers each should have their own class and `configure()` override. I've also never seen the `RememberMeServices` so I'll check it out. – J-Deq87 Oct 29 '21 at 17:26
  • For multiple auth providers each should have their own class and configure() override. This is correct. – Tarun Singh Rawat Oct 29 '21 at 21:07
  • SO what you want to ask exactly, please clarify – Tarun Singh Rawat Oct 29 '21 at 21:07
  • Tarun - Sorry for the delay in response. I think the main issue is that whenever I introduce a new provider/rememberMeService that _always_ authenticates to true, just for the sake of testing, then _everything_ breaks. Keycloak requests return a 401 and my new provider that always does `auth=true` and returns a 400. Something about the Keycloak Valve that is a low-level pipeline before we even reach filters might have something to do with it. I just can't find a simple example with Keycloak and another custom Provider. I'll keep tinkering and going through the docs. – J-Deq87 Nov 02 '21 at 16:53