Securing a Microservice with Keycloak

keycloak

In the previous article we saw that there are 3 spring-boot micro-services and each of them are protected by keycloak adapter built on top of spring security .

We will be using OpenID connect (OIDC) protocol which is an extension of Oauth2. The user authenticated information is digitally signed and encrypted in a JSON format called JWT. First the client application will ask the Keycloak server to authenticate the user. After successful authentication, the client application will receive identity token and access token . The identity token has the user info and access token will have the digitally signed realm access information like role mapping or in one word Authorization info .

If you have any external ldap or identity provider , you need to go to identity providers tab and do the configuration . Here I am creating a custom user to authenticate

Create an user : service-user

Creating client in Keycloak.

Access Type should be confidential . Confidential means it will need a secret to initate a login . We will have the secret details in credential tab there . Client needs to pass the secret to generate the access token .

Make sure to mark the service account enabled as that is required for service to service communication Also mark the callback url (https://openidconnect.net/callback )which needs to be passed to generate the token . 

Add a role: ROLE_SERVICE

Now go to the service-user we created earlier and map the role in the client-roles:

So now service-user belongs to ROLES_SERVICE and should be able to access service-client.

Keycloak Configuration in spring boot

Let’s secure recipe-service and recipe-cost spring boot app with keycloak and below is the sequence of data between the 2 service :

Below configuration is applicable for both recipe-service and cost-service

  • Add the keycloak dependency in pom 
   <dependency>
       <groupId>org.keycloak</groupId>
       <artifactId>keycloak-spring-boot-starter</artifactId>
       <version>6.0.1</version>
   </dependency>
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-security</artifactId>
   </dependency>   
  • Add the annotation in config class 
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
@KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter implements WebMvcConfigurer {
......
  • Include the keycloak authentication  filters
@Bean
	public FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean(
			KeycloakAuthenticationProcessingFilter filter) {
		FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
		registrationBean.setEnabled(false);
		return registrationBean;
	}

	@Bean
	public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean(KeycloakPreAuthActionsFilter filter) {
		FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
		registrationBean.setEnabled(false);
		return registrationBean;
	}

	@Bean
	public FilterRegistrationBean keycloakAuthenticatedActionsFilterBean(KeycloakAuthenticatedActionsFilter filter) {
		FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
		registrationBean.setEnabled(false);
		return registrationBean;
	}

	@Bean
	public FilterRegistrationBean keycloakSecurityContextRequestFilterBean(
			KeycloakSecurityContextRequestFilter filter) {
		FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
		registrationBean.setEnabled(false);
		return registrationBean;
	}
  • Add keycloak config resolver:
@Bean
public KeycloakConfigResolver KeycloakConfigResolver() {
		return new KeycloakSpringBootConfigResolver();
	}
  • Add the session authentication strategy and keycloak authentication manager 
     @Autowired
     public void configureGlobal(AuthenticationManagerBuilder auth) 
     throws Exception {
         auth.authenticationProvider(keycloakAuthenticationProvider());
     }
 
     /**
      * Defines the session authentication strategy.
      */
     @Bean
     @Override
     protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
         return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
     }
  • Add the KeycloakClientRequestFactory bean for defining keycloak rest template calls . This will automatically add the token in the headers of rest template calls
@Autowired
public KeycloakClientRequestFactory keycloakClientRequestFactory;
 
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public KeycloakRestTemplate keycloakRestTemplate() {
    return new KeycloakRestTemplate(keycloakClientRequestFactory);
}
  • Below details needs to be added in application.properties for both project . Change the client and realm names as required in the project . I am referring to the client and roles I created in above steps .
keycloak.auth-server-url=https://sso-keycloak.geeks18/auth
keycloak.realm=Staging-Realm
keycloak.resource=service-client
keycloak.bearer-only=true
keycloak.use-resource-role-mappings=true
keycloak.security-constraints[0].authRoles[0]=service-role
keycloak.cors=true
spring.main.allow-bean-definition-overriding=true

Changes required for recipe-service only :

Below method is for spring security authorization . This will only allow request coming to /geeks18/recipe only if user has SERVICE role . Remember the role defined for the user in Keycloak server . It was ROLE_SERVICE . Spring security automatically appends ROLE_before every role .

@Override
protected void configure(HttpSecurity http) throws Exception {
		super.configure(http);
		http.authorizeRequests().antMatchers("/geeks18/recipe/**")
				.hasRole("SERVICE").anyRequest().denyAll();

		http.csrf().disable();
		http.headers().frameOptions().disable();

	}

Add the below code in RecipeService class which will call the cost-service api to retrieve the cost of the recipe :

Inject the KeycloakRestTemplate

@Autowired
KeycloakRestTemplate keycloakRestTemplate;
Double cost = keycloakRestTemplate.postForEntity("http://recipe-cost:9001/geeks18/cost/",ingredients, Double.class);

Please note that recipe-cost is the service name of recipe-cost microservice in openshift .We will be calling it not by the url rather the name of the service . Openshift itself will do the service discovery for us and map it to appropriate internal load balancer url .

Change required for cost-service :

controller class will look like this :

@RestController
@RequestMapping("/geeks18")
public class CostController {

	@RequestMapping(value = "/cost", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
	public @ResponseBody ResponseEntity<Double> getCostOfRecipe(HttpServletRequest request,
			@RequestBody List<Ingredient> ingredientList) {
.....
....

Below change is in configuration class for spring security authorization :

@Override
protected void configure(HttpSecurity http) throws Exception {
		super.configure(http);
http.authorizeRequests().antMatchers("/geeks18/cost/**").hasRole("SERVICE").anyRequest().denyAll();
		http.csrf().disable();
	}

Start both the server in local , make sure to update the port differently in local and update the service call from recipe-service to recipe-cost like below :

Double cost = keycloakRestTemplate.postForEntity("http://localhost:9001/geeks18/cost/",ingredients, Double.class);

Validate from postman

We will first generate the access token and then hit the recipe-service endpoint with the token in header which will internally get validated in keycloak and then pass on the request and token to recipe-cost .


That’s it . Now your spring boot app is integrated successfully with Keycloak. In next article we will see how we can integrate it with Angular App.

In the next section we will deploy the spring boot micro-service in Openshift with Jenkins .

Digiprove sealCopyright secured by Digiprove © 2020 Geeks 18

Be the first to comment

Leave a Reply

Your email address will not be published.


*