DEV Community

Cover image for Add Keycloak in legacy Spring app
Ulrich VACHON
Ulrich VACHON

Posted on

Add Keycloak in legacy Spring app

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"));
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

💡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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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"}
Enter fullscreen mode Exit fullscreen mode

🔴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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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));
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

🎉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"}
Enter fullscreen mode Exit fullscreen mode

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)