Integrating Keycloak and Spring Security with Basic Authentication.

In this guide, we’ll explore integrating Spring Boot (with Spring Security) and Keycloak using basic authentication flow.

I found this approach particularly useful when creating a server acting as a Maven repository. To ensure smooth interaction with the Gradle client, it needed to gracefully handle authentication attempts without an authentication header (respond with a 401 Unauthorized and a correct WWW-Authenticate header), keep the connection open, and handle the next request containing an Authorization header with basic authentication.

Key Insight: This setup runs as bearer-only, but we don’t want to be redirected to the login page that Keycloak provides.

This configuration has been tested against Keycloak 8.0.1.

Using Spring Boot 2.3.4.RELEASE, the following dependencies are used:

implementation platform("org.keycloak.bom:keycloak-adapter-bom:4.8.3.Final")
implementation "org.springframework.boot:spring-boot-starter-security"
implementation "org.keycloak:keycloak-spring-boot-2-adapter"
implementation "org.keycloak:spring-boot-container-bundle"

Create a configuration class that looks something like this:

@KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http.authorizeRequests()
                .antMatchers("/actuator/health").permitAll()
                .antMatchers("/actuator/info").permitAll()
                .anyRequest().authenticated();
    }

    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new NullAuthenticatedSessionStrategy();
    }
    
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) {
        KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
        auth.authenticationProvider(keycloakAuthenticationProvider);
    }
}

This configures the Keycloak integration, securing all endpoints except /actuator/health and /actuator/info. The sessionAuthenticationStrategy method is overridden to use NullAuthenticatedSessionStrategy since no sessions are used (stateless auth). Additionally, the KeycloakAuthenticationProvider is configured to use the SimpleAuthorityMapper, allowing easy use of roles configured in Keycloak as authorities in Spring Security.

Important: Note the super.configure(http) call in the configure method.

Spring Boot Configuration

When using Spring Boot, add the following configuration to this class:

    @Bean
    public KeycloakConfigResolver keycloakConfigResolver() {
        return new KeycloakSpringBootConfigResolver();
    }

    // https://issues.redhat.com/browse/KEYCLOAK-8725
    @Bean
    @Override
    @ConditionalOnMissingBean(HttpSessionManager.class)
    protected HttpSessionManager httpSessionManager() {
        return new HttpSessionManager();
    }

    // Needed because Spring Boot eagerly registers filter beans to the web application context. This prevents them being registered twice.
    @Bean
    public FilterRegistrationBean<KeycloakAuthenticationProcessingFilter> keycloakAuthenticationProcessingFilterRegistrationBean(
            KeycloakAuthenticationProcessingFilter filter) {
        FilterRegistrationBean<KeycloakAuthenticationProcessingFilter> registrationBean = new FilterRegistrationBean<>(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

    // Needed because Spring Boot eagerly registers filter beans to the web application context. This prevents them being registered twice.
    @Bean
    public FilterRegistrationBean<KeycloakPreAuthActionsFilter> keycloakPreAuthActionsFilterRegistrationBean(
            KeycloakPreAuthActionsFilter filter) {
        FilterRegistrationBean<KeycloakPreAuthActionsFilter> registrationBean = new FilterRegistrationBean<>(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

    // Needed because Spring Boot eagerly registers filter beans to the web application context. This prevents them being registered twice.
    @Bean
    public FilterRegistrationBean<KeycloakAuthenticatedActionsFilter> keycloakAuthenticatedActionsFilterBean(
            KeycloakAuthenticatedActionsFilter filter) {
        FilterRegistrationBean<KeycloakAuthenticatedActionsFilter> registrationBean = new FilterRegistrationBean<>(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

    // Needed because Spring Boot eagerly registers filter beans to the web application context. This prevents them being registered twice.
    @Bean
    public FilterRegistrationBean<KeycloakSecurityContextRequestFilter> keycloakSecurityContextRequestFilterBean(
            KeycloakSecurityContextRequestFilter filter) {
        FilterRegistrationBean<KeycloakSecurityContextRequestFilter> registrationBean = new FilterRegistrationBean<>(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

The KeycloakSpringBootConfigResolver allows Keycloak configuration properties to be specified as regular Spring properties instead of using a keycloak.json configuration file.

These properties are used to configure the Keycloak integration at runtime:

keycloak.ssl-required=external
keycloak.principal-attribute=preferred_username

keycloak.auth-server-url=https://your.keycloak.url/auth
keycloak.realm=your-keycloak-realm
keycloak.public-client=false
keycloak.resource=your-keycloak-client-name
keycloak.credentials.secret=your-client-secret

keycloak.bearer-only=true
keycloak.enable-basic-auth=true

To allow basic authentication, set keycloak.enable-basic-auth to true, and set keycloak.bearer-only to true to disable redirects to the Keycloak provided login page.

If we connect with an Authorization: Basic header this works, but if we do not we get an 401 Unauthorized but with the wrong WWW-Authenticate header. Due to this issue I could not get a proper interaction between my repository and my Gradle client. This is because KeycloakAuthenticationEntryPoint is implemented like:

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        HttpFacade facade = new SimpleHttpFacade(request, response);
        if (apiRequestMatcher.matches(request) || adapterDeploymentContext.resolveDeployment(facade).isBearerOnly()) {
            commenceUnauthorizedResponse(request, response);
        } else {
            commenceLoginRedirect(request, response);
        }
    }

    protected void commenceLoginRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String contextAwareLoginUri = request.getContextPath() + loginUri;
        log.debug("Redirecting to login URI {}", contextAwareLoginUri);
        response.sendRedirect(contextAwareLoginUri);
    }

    protected void commenceUnauthorizedResponse(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.addHeader(HttpHeaders.WWW_AUTHENTICATE, String.format("Bearer realm=\"%s\"", realm));
        response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
    }

Since we have to set keycloak.bearer-only to true to not be redirected, we get a 401 Unauthorized with WWW-Authenticate: Bearer realm=”your-keycloak-realm”. In our use-case this should be: WWW-Authenticate: Basic realm=”your-keycloak-realm”, however the Bearer part is hardcoded in the keycloak spring security adapter.

Custom AuthenticationEntryPoint

To address this issue, implement a custom AuthenticationEntryPoint based on the existing Keycloak one:

    @Override
    protected AuthenticationEntryPoint authenticationEntryPoint() throws Exception {
        return new KeycloakWithBasicAuthAuthenticationEntryPoint(adapterDeploymentContext());
    }
public class KeycloakWithBasicAuthAuthenticationEntryPoint extends KeycloakAuthenticationEntryPoint {

    private String realm = "Unknown";

    public KeycloakWithBasicAuthAuthenticationEntryPoint(AdapterDeploymentContext adapterDeploymentContext) {
        super(adapterDeploymentContext);
    }

    public KeycloakWithBasicAuthAuthenticationEntryPoint(AdapterDeploymentContext adapterDeploymentContext, RequestMatcher apiRequestMatcher) {
        super(adapterDeploymentContext, apiRequestMatcher);
    }

    @Override
    protected void commenceUnauthorizedResponse(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.addHeader(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\""+realm+"\"");
        response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
    }

    @Override
    public void setRealm(String realm) {
        Assert.notNull(realm, "realm cannot be null");
        this.realm = realm;
    }
}

Full configuration

@KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http.authorizeRequests()
                .antMatchers("/actuator/health").permitAll()
                .antMatchers("/actuator/info").permitAll()
                .anyRequest().authenticated();
    }

    @Override
    protected AuthenticationEntryPoint authenticationEntryPoint() throws Exception {
        return new KeycloakWithBasicAuthAuthenticationEntryPoint(adapterDeploymentContext());
    }

    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new NullAuthenticatedSessionStrategy();
    }

    @Bean
    public KeycloakConfigResolver keycloakConfigResolver() {
        return new KeycloakSpringBootConfigResolver();
    }

    // https://issues.redhat.com/browse/KEYCLOAK-8725
    @Bean
    @Override
    @ConditionalOnMissingBean(HttpSessionManager.class)
    protected HttpSessionManager httpSessionManager() {
        return new HttpSessionManager();
    }

    // Needed because Spring Boot eagerly registers filter beans to the web application context. This prevents them being registered twice.
    @Bean
    public FilterRegistrationBean<KeycloakAuthenticationProcessingFilter> keycloakAuthenticationProcessingFilterRegistrationBean(
            KeycloakAuthenticationProcessingFilter filter) {
        FilterRegistrationBean<KeycloakAuthenticationProcessingFilter> registrationBean = new FilterRegistrationBean<>(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

    // Needed because Spring Boot eagerly registers filter beans to the web application context. This prevents them being registered twice.
    @Bean
    public FilterRegistrationBean<KeycloakPreAuthActionsFilter> keycloakPreAuthActionsFilterRegistrationBean(
            KeycloakPreAuthActionsFilter filter) {
        FilterRegistrationBean<KeycloakPreAuthActionsFilter> registrationBean = new FilterRegistrationBean<>(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

    // Needed because Spring Boot eagerly registers filter beans to the web application context. This prevents them being registered twice.
    @Bean
    public FilterRegistrationBean<KeycloakAuthenticatedActionsFilter> keycloakAuthenticatedActionsFilterBean(
            KeycloakAuthenticatedActionsFilter filter) {
        FilterRegistrationBean<KeycloakAuthenticatedActionsFilter> registrationBean = new FilterRegistrationBean<>(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

    // Needed because Spring Boot eagerly registers filter beans to the web application context. This prevents them being registered twice.
    @Bean
    public FilterRegistrationBean<KeycloakSecurityContextRequestFilter> keycloakSecurityContextRequestFilterBean(
            KeycloakSecurityContextRequestFilter filter) {
        FilterRegistrationBean<KeycloakSecurityContextRequestFilter> registrationBean = new FilterRegistrationBean<>(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }
}