Keycloak config for Java application with Spring (Java 11)

 

If you want to secure your Spring microservice endpoints with Keycloak, you are in perfect place. Let’s do this step by step.


First we need is of course to add proper dependencies, I’m using maven:
<dependencies>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-spring-boot-starter</artifactId>
            <version>${keycloak.version}</version>
        </dependency>

        <dependency>
            <groupId>org.keycloak.bom</groupId>
            <artifactId>keycloak-adapter-bom</artifactId>
            <version>${keycloak.version}</version>
            <type>pom</type>
        </dependency>

        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-spring-boot-adapter-core</artifactId>
            <version>${keycloak.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
</dependencies>


Second, provide some properties for our authorization server connection:
keycloak:
  auth-server-url: http://localhost:8080
  realm: custom-realm
  resource: custom-client
  bearer-only: true
  use-resource-role-mappings: false
  public-client: true


Third and last – create a configuration class. Good practice is to keep your endpoints authorized by default (as provided below). If you are not using CORS you can skip lines 50-51.
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.KeycloakConfiguration;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;

@KeycloakConfiguration
@Import(KeycloakSpringBootConfigResolver.class)
@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class KeycloakConfig extends KeycloakWebSecurityConfigurerAdapter {

    /**
     * Registers the KeycloakAuthenticationProvider with the authentication manager.
     */
    @Autowired
    protected void configure(AuthenticationManagerBuilder auth) {
        KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
        auth.authenticationProvider(keycloakAuthenticationProvider);
    }

    /**
     * Defines the session authentication strategy.
     */
    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
    }

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

                // unsecured endpoints
                .antMatchers("/example/unsecured").permitAll()

                //for preflights
                .antMatchers(HttpMethod.OPTIONS)
                .permitAll()

                //default authorized
                .anyRequest()
                .hasRole("default-roles-custom-realm");

        http.csrf().disable();
    }
}








Extras


Getting user information

Template for getting user info:
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.keycloak.KeycloakPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class UserUtils {

    public static String getActualUserId() {
        return ((KeycloakPrincipal<?>) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getName();
    }

    public static String getActualUserAuthToken() {
        return ((KeycloakPrincipal<?>) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getKeycloakSecurityContext().getToken().getType() +
                StringUtils.SPACE +
                ((KeycloakPrincipal<?>) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getKeycloakSecurityContext().getTokenString();
    }

    public static String getActualUserIdOrEmpty() {
        try {
            return ((KeycloakPrincipal<?>) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getName();
        } catch (Exception e) {
            return StringUtils.EMPTY;
        }
    }
}




Profiled security

If you are using profiles for your app and for example want to unsecure specific endpoints on dev which are usually disabled/forbidden on prod (i.e. microservice API documentation like Swagger/OpenAPI) you can create an Interface and provide different implementation per each profile.


Interface will be very simple:
import org.springframework.security.config.annotation.web.builders.HttpSecurity;

public interface ISwaggerPathMatcher {
    void match(HttpSecurity http) throws Exception;
}


Configuration file will then contain a Bean with this interface type:
import lombok.RequiredArgsConstructor;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.KeycloakConfiguration;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;

@KeycloakConfiguration
@RequiredArgsConstructor
@Import(KeycloakSpringBootConfigResolver.class)
@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class KeycloakConfig extends KeycloakWebSecurityConfigurerAdapter {

    private final ISwaggerPathMatcher swaggerPathMatcher;
    
    @Autowired
    protected void configure(AuthenticationManagerBuilder auth) {
        KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
        auth.authenticationProvider(keycloakAuthenticationProvider);
    }
    
    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
    }

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

                // unsecured endpoints
                .antMatchers("/unsecured/endpoint").permitAll()
                .antMatchers("/unsecured/path/*").permitAll()

                .antMatchers(HttpMethod.OPTIONS)
                .permitAll()
                .anyRequest()
                .hasRole("default-roles-custom-realm");

        http.csrf().disable();
    }
}


Example implementation for dev profile looks this way:
import org.springframework.context.annotation.Profile;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.stereotype.Component;


@Profile("dev")
@Component
public class DevSwaggerPathMatcherImpl implements ISwaggerPathMatcher {
    @Override
    public void match(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/webjars/**").permitAll()
                .antMatchers("/swagger-resources/**").permitAll()
                .antMatchers("/swagger-resources").permitAll()
                .antMatchers("/v2/api-docs").permitAll()
                .antMatchers("/swagger-ui.html").permitAll();
    }
}


Example of prod profile Bean:
import org.springframework.context.annotation.Profile;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.stereotype.Component;

@Profile("!dev")
@Component
public class DefaultSwaggerPathMatcher implements ISwaggerPathMatcher {
    @Override
    public void match(HttpSecurity http) throws Exception {
    }
}




Working with tokens with curl

Getting access token:
curl -v -X POST http://localhost:8080/realms/custom-realm/protocol/openid-connect/token -H 'Content-Type:application/x-www-form-urlencoded' --data "client_id=custom-client&username=user&password=user&grant_type=password"
Refreshing token:
curl -v --data "grant_type=refresh_token&client_id=custom-client&refresh_token={REFRESH_TOKEN}" http://localhost:8080/realms/custom-realm/protocol/openid-connect/token


Popular posts

Basic AKHQ running with docker (HTTP)

Running keycloak in 4 minutes for development on Windows

AKHQ login with Keycloak