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