Today I will show you how I added Keycloak in existing Spring app.
Often when we want to study some technology, it's easy to ignit the stack by running the corresponding hello-world 🌎 sample from sources, documentation, blog post, hands-on...
But as you say in the real life (project), the things are never straightforward. OK it said.
Thereby this article we'll discover an application which already being protected by an existing security layer but which needs to use Keycloak for the next generation of its controllers. So the challenge for us there is to do work together the existing security layer with the newer based on Keycloak.
The existing code
For the demonstration we use a very simple security pattern based on the Authorization
header presence. You can imagine that this security being based on a custom JWT layer or anything else...
@RestController
@RequestMapping(value = "/api/v1/user")
public class UserControllerImpl {
@GetMapping
public ResponseEntity<User> getUser(@RequestHeader("Authorization") String authorization, String id) {
var tokenOpt = Optional.ofNullable(authorization);
if (tokenOpt.isEmpty()) {
return ResponseEntity
.status(BAD_REQUEST)
.build();
}
var token = tokenOpt.get();
if (!"authorization".equals(token)) {
return ResponseEntity
.status(UNAUTHORIZED)
.build();
}
return ResponseEntity
.ok(new User("1", "Ulrich"));
}
}
As expected I will call my controller with CURL command and the result shall be very simple to understand.
❯ curl -H "Authorization: foo" --verbose http://localhost:9999/api/v1/user
* Trying 127.0.0.1:9999...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 9999 (#0)
> GET /api/v1/user HTTP/1.1
> Host: localhost:9999
> User-Agent: curl/7.68.0
> Accept: */*
> Authorization: foo
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 401
< Content-Length: 0
< Date: Sun, 22 May 2022 14:03:49 GMT
<
* Connection #0 to host localhost left intact
Add Keycloak security layer
This part won't be detailed because it's only some configurations to add in your Maven project and Spring Security layer, but broadly we have to follow the steps :
Add Spring Security feature
This is the start of migration and for that we have to add some dependencies in our project as Spring Security configuration to bu use by the existing resources (/api/v1).
Keep in mind that we don't want to substitute the existing security layer by Spring Security and Keycloak but we want to dedicate a Spring Security configuration to the existing resources. Nothing else.
@Configuration
@EnableWebSecurity
@ConditionalOnProperty(value = "app.keycloak.enabled", havingValue = "false", matchIfMissing = true)
public class LegacySecurityConfig extends WebSecurityConfigurerAdapter {
private static final Logger logger = LoggerFactory.getLogger(LegacySecurityConfig.class);
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
logger.info("Configuring the legacy security layer");
httpSecurity.csrf().disable();
httpSecurity.authorizeRequests().anyRequest().permitAll();
}
}
We will test this configuration by a CURL command to check if the existing resource can be reached again and also search the entry log related to the initialization of this configuration.
2022-05-22 16:31:18.102 INFO 34438 --- [main] c.r.s.config.LegacySecurityConfig : Configuring the legacy security layer
It's works👌
Add Keycloak feature
In this part of the migration I supposed that you have an Keycloak instance already set with a realm named test
. If this is not the case you can start an instance based on my project located in the GitHub project.
Declare the KeycloakConfigResolver component
The goal of this component is to create a deployment
in the Keycloak terminology. The deployment bean stores the configuration claimed by the combo Spring Security and Keycloak library.
@Configuration
@Component("keycloakConfigResolver")
public class SimpleKeycloakConfigResolver implements KeycloakConfigResolver {
private final static Logger logger = LoggerFactory.getLogger(SimpleKeycloakConfigResolver.class);
@Value("${app.keycloak.server_url}")
private String keycloakServerUrl;
@Value("${app.keycloak.config_ssl}")
private String keycloakConfigSSL;
@Value("${app.keycloak.config_realm}")
private String keycloakConfigRealm;
@Value("${app.keycloak.config_resource}")
private String keycloakConfigResource;
@Override
public KeycloakDeployment resolve(OIDCHttpFacade.Request request) {
logger.info("Configuring the Keycloak deployment");
var adapterConfig = new AdapterConfig();
adapterConfig.setBearerOnly(true);
adapterConfig.setRealm(keycloakConfigRealm);
adapterConfig.setSslRequired(keycloakConfigSSL);
adapterConfig.setResource(keycloakConfigResource);
adapterConfig.setAuthServerUrl(keycloakServerUrl);
return KeycloakDeploymentBuilder
.build(adapterConfig);
}
}
💡As you can image we can have as much deployments as we want and by definition having a multitenant app capability.
Declare the KeycloakSecurityConfig component
This component is the entry point of the Keycloak configuration and allows developer to customize the behavior of sub components able to manage several things as the requests, the CORS or CSRF configurations etc...
The configure
method is the place where we declare the Http security configuration and specially which one URL have to be handled by Keycloak.
@Configuration
@EnableWebSecurity
@KeycloakConfiguration
@DependsOn("keycloakConfigResolver")
@ConditionalOnProperty(value = "app.keycloak.enabled", havingValue = "true")
public class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
private static final Logger logger = LoggerFactory.getLogger(LegacySecurityConfig.class);
public static final String VALIDATING_BY_KEYCLOAK = "/api/v2/**";
@Autowired
public void authenticationProvider(AuthenticationManagerBuilder authenticationManagerBuilder) {
authenticationManagerBuilder.authenticationProvider(keycloakAuthenticationProvider());
}
@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
logger.info("Configuring the Keycloak security layer");
super.configure(httpSecurity);
httpSecurity.cors();
httpSecurity.csrf().disable();
httpSecurity.authorizeRequests().antMatchers(VALIDATING_BY_KEYCLOAK).authenticated();
httpSecurity.authorizeRequests().anyRequest().permitAll();
}
}
If we take the time to test the global mechanism we see that configuration runs well and that the /api/v2/user
endpoit as well protected, awesome!
🟢Here we use a valid access token for test :
❯ curl -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItNFZiQmhtN1pZaXlhckdSMjk0el8xRDIwYzA5Y2Vrc0V4ZHJmUW9ESnhNIn0.eyJleHAiOjE2NTMyNDAzMjcsImlhdCI6MTY1MzI0MDAyNywianRpIjoiMjJlN2E4NTgtMDJhYS00NzE4LTlkZGItMDUwMDkwMmEyN2NiIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL3Rlc3QiLCJzdWIiOiI5OTZmMDU2My04OGExLTQwYjEtYTc2NS0xNmI1NDYyMjBjZWYiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJhZG1pbi1jbGkiLCJzZXNzaW9uX3N0YXRlIjoiNGExZDcyODEtZjk5OC00NGQ0LTliYmQtZDRkNmM4ZjhhNDJlIiwiYWNyIjoiMSIsInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsInNpZCI6IjRhMWQ3MjgxLWY5OTgtNDRkNC05YmJkLWQ0ZDZjOGY4YTQyZSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoidWxyaWNoIn0.h26O4nyVIpYPIzN5ocgvtBCRHcjEGT9kCxkv5T0yniLeTYU2XE0GZp_dmmb383sPz1KNJPOZRCYqMOgRFBl-7l9yWbiW85CsJ9EBsKDsp6z1lgtg_hkH0VDi_yVgDcdtQQ5fr7RppeoOhvmlM39qYf4_H2dr4WvhnJnjqLRBDsxcEaFWB97W8CUG66Ng2qEaHpmsqwFP7tPOdkwjktY9iBHJzG85SqKYO6YHbMM6YvukT14EwH2lFI4l-LpSfq1kXCMjZ9M__YEBbYswHs9RXfubI_Myr1kwCxFkXiqG6JOlwrxP8-UIRqTu2BCmbyq5YeliDhzO0NZP0eMTsaAedw' --verbose http://localhost:9999/api/v2/user
* Trying 127.0.0.1:9999...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 9999 (#0)
> GET /api/v2/user HTTP/1.1
> Host: localhost:9999
> User-Agent: curl/7.68.0
> Accept: */*
> Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItNFZiQmhtN1pZaXlhckdSMjk0el8xRDIwYzA5Y2Vrc0V4ZHJmUW9ESnhNIn0.eyJleHAiOjE2NTMyNDAzMjcsImlhdCI6MTY1MzI0MDAyNywianRpIjoiMjJlN2E4NTgtMDJhYS00NzE4LTlkZGItMDUwMDkwMmEyN2NiIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL3Rlc3QiLCJzdWIiOiI5OTZmMDU2My04OGExLTQwYjEtYTc2NS0xNmI1NDYyMjBjZWYiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJhZG1pbi1jbGkiLCJzZXNzaW9uX3N0YXRlIjoiNGExZDcyODEtZjk5OC00NGQ0LTliYmQtZDRkNmM4ZjhhNDJlIiwiYWNyIjoiMSIsInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsInNpZCI6IjRhMWQ3MjgxLWY5OTgtNDRkNC05YmJkLWQ0ZDZjOGY4YTQyZSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoidWxyaWNoIn0.h26O4nyVIpYPIzN5ocgvtBCRHcjEGT9kCxkv5T0yniLeTYU2XE0GZp_dmmb383sPz1KNJPOZRCYqMOgRFBl-7l9yWbiW85CsJ9EBsKDsp6z1lgtg_hkH0VDi_yVgDcdtQQ5fr7RppeoOhvmlM39qYf4_H2dr4WvhnJnjqLRBDsxcEaFWB97W8CUG66Ng2qEaHpmsqwFP7tPOdkwjktY9iBHJzG85SqKYO6YHbMM6YvukT14EwH2lFI4l-LpSfq1kXCMjZ9M__YEBbYswHs9RXfubI_Myr1kwCxFkXiqG6JOlwrxP8-UIRqTu2BCmbyq5YeliDhzO0NZP0eMTsaAedw
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200
< Vary: Origin
< Vary: Access-Control-Request-Method
< Vary: Access-Control-Request-Headers
< Set-Cookie: JSESSIONID=89CE06C8B841F99D3F161C4E3D1FE8A5; Path=/; HttpOnly
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Sun, 22 May 2022 17:21:07 GMT
<
* Connection #0 to host localhost left intact
{"id":"1","name":"Ulrich"}
🔴Here we use an invalid access token for test :
❯ curl -H 'Authorization: MY_BEARER_TOKEN_IS_WRONG' --verbose http://localhost:9999/api/v2/user
* Trying 127.0.0.1:9999...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 9999 (#0)
> GET /api/v2/user HTTP/1.1
> Host: localhost:9999
> User-Agent: curl/7.68.0
> Accept: */*
> Authorization: MY_BEARER_TOKEN_IS_WRONG
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 401
< Vary: Origin
< Vary: Access-Control-Request-Method
< Vary: Access-Control-Request-Headers
< WWW-Authenticate: Bearer realm="test"
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< WWW-Authenticate: Bearer realm="test"
< Content-Length: 0
< Date: Sun, 22 May 2022 17:24:50 GMT
<
* Connection #0 to host localhost left intact
We have to confirm that the previous /api/v1/user
resource works as before but unfortunately it's broken.
🔴Here we can confirm something was broken :
❯ curl -H "Authorization: authorization" --verbose http://localhost:9999/api/v1/user
* Trying 127.0.0.1:9999...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 9999 (#0)
> GET /api/v1/user HTTP/1.1
> Host: localhost:9999
> User-Agent: curl/7.68.0
> Accept: */*
> Authorization: authorization
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 401
< Vary: Origin
< Vary: Access-Control-Request-Method
< Vary: Access-Control-Request-Headers
< WWW-Authenticate: Bearer realm="test"
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< WWW-Authenticate: Bearer realm="test"
< Content-Length: 0
< Date: Sun, 22 May 2022 17:28:55 GMT
<
* Connection #0 to host localhost left intact
Declare the KeycloakAuthenticationProcessingFilter component
The reason behind the previous issue is related to the way of Spring Security interacts with the Keycloak library. Keycloak takes the hand for all incoming requests and we have to declare we don't want handle all requests but only the /api/v2/user
based requests.
For that, we have only to override the KeycloakAuthenticationProcessingFilter
bean by adding a specific matcher which match only for the /api/v2/*
requests. Please take a look :
public class CustomKeycloakAuthenticationProcessingFilter extends KeycloakAuthenticationProcessingFilter {
public CustomKeycloakAuthenticationProcessingFilter(AuthenticationManager authenticationManager) {
super(authenticationManager, new AntPathRequestMatcher(VALIDATING_BY_KEYCLOAK));
}
}
To achieve this overloading we have to improve the existing KeycloakSecurityConfig
bean.
@Configuration
@EnableWebSecurity
@KeycloakConfiguration
@DependsOn("keycloakConfigResolver")
@ConditionalOnProperty(value = "app.keycloak.enabled", havingValue = "true")
public class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
private static final Logger logger = LoggerFactory.getLogger(LegacySecurityConfig.class);
public static final String VALIDATING_BY_KEYCLOAK = "/api/v2/**";
@Autowired
public void authenticationProvider(AuthenticationManagerBuilder authenticationManagerBuilder) {
authenticationManagerBuilder.authenticationProvider(keycloakAuthenticationProvider());
}
@Bean
@Override
protected KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception {
return new CustomKeycloakAuthenticationProcessingFilter(authenticationManagerBean());
}
@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
logger.info("Configuring the Keycloak security layer");
super.configure(httpSecurity);
httpSecurity.cors();
httpSecurity.csrf().disable();
httpSecurity.authorizeRequests().antMatchers(VALIDATING_BY_KEYCLOAK).authenticated();
httpSecurity.authorizeRequests().anyRequest().permitAll();
}
}
🎉Here we are testing the /api/v1/user
call with Keycloak enabled :
❯ curl -H "Authorization: authorization" --verbose http://localhost:9999/api/v1/user
* Trying 127.0.0.1:9999...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 9999 (#0)
> GET /api/v1/user HTTP/1.1
> Host: localhost:9999
> User-Agent: curl/7.68.0
> Accept: */*
> Authorization: authorization
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200
< Vary: Origin
< Vary: Access-Control-Request-Method
< Vary: Access-Control-Request-Headers
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Sun, 22 May 2022 17:57:56 GMT
<
* Connection #0 to host localhost left intact
{"id":"1","name":"Ulrich"}
Let me try it
If you want a complete Spring boot example with the Keycloak integration as expected in this article, you can clone the repository https://github.com/ulrich/spring-legacy-and-keycloak
Crédit photo : https://pixabay.com/fr/users/jackmac34-483877/
Top comments (0)