Secure Microservices with Keycloak Client Credentials Flow

Secure Microservices with Keycloak Client Credentials Flow

Keycloak Client Credentials

1. Introduction

In this tutorial, we’ll explore the process of establishing secure authentication between Spring Boot microservices by using Keycloak OAuth2.0 Client Credentials flow. The Client Credentials Flow is a server-to-server authentication mechanism, ideal for scenarios where one application needs to send requests to another without user involvement, such as between two microservices.

1.1 How Client Credentials Flow Works:

  • To initiate the flow, the client application sends an HTTP POST request to the token endpoint of the authorization server.
  • This request includes parameters such as grant_type, client_id, client_secret, and scope.
  • Upon successful validation, the server promptly responds with an access token.
  • With this access token in hand, the client can now seamlessly send requests to the resource server.

2. Setting Up Keycloak

To get started, first, we need to install Keycloak on our system. In this example, we are using Keycloak v22.

Begin by downloading the latest version of Keycloak from the official website. Once the download is complete, unzip the Keycloak archive. Next, navigate to the bin directory and run the following command:

./kc.sh start-dev

After starting the Keycloak instance, open your web browser and navigate to http://localhost:8080/.

Under the Administration Console section, create an admin account with the username ‘admin’ and the password ‘admin’.

Screenshot-2023-11-02-at-8.01.12-AM Secure Microservices with Keycloak Client Credentials Flow

2.1 Creating a Realm

Realms in Keycloak define a space for users, roles, and applications.

  1. In the top left corner, click on the dropdown next to Master and then click on Create Realm.
  2. For this tutorial, we named the realm as FullStackApp.
  3. Click on the Create button.

2.2 Creating a Client

Once we have our realm, let’s start creating Keycloak clients.

The backend client is usually configured as “bearer-only.” It doesn’t initiate authentication flows but expects every incoming request to present a valid bearer token (JWT). This token is then validated to ensure that the request is both authenticated and authorized.

  1. Navigate to the desired realm FullStackApp
  2. Go to the clients section and click on Create client.
  3. Choose OpenID Connect the client type.
  4. Enter a unique Client ID , we named it fullstack-springboot-client. This will be used in the spring-boot application.
  5. Turn On the client authentication. When it is on, it is set to confidential access type, client secret will be generated.
  6. Turn On the Authorization to enable fine-grained authorization support. It means the client can use OAuth 2.0 scopes to request specific permissions.
  7. Select the Service accounts roles the Authentication flow.
  8. Click on the Create button.

3. Secure Spring Boot Microservices

In this section, we will configure the order-service to communicate with the product-service using Feign client, and we’re also integrating Keycloak for authentication using Feign Configuration.

3.1 Update application.properties with Keycloak Configurations:

In the order-service application.properties file, we have to provide the Keycloak configurations. These configurations are essential for the order microservice to authenticate and obtain access tokens from the Keycloak server.

keycloak.client-id=fullstack-springboot-client
keycloak.client-secret=[CLIENT_SECRET]
keycloak.token-uri=http://localhost:8080/realms/FullStackApp/protocol/openid-connect/token

3.2 Modify the Feign Client to Include an OAuth2 Interceptor:

Create a FeignConfig class that will be used to create a Feign interceptor. This interceptor will add an OAuth2 bearer token to your requests, ensuring secure communication with the product microservice. The @Configuration annotation marks this class as a configuration class.

@Configuration
public class FeignConfig {
    @Value("${keycloak.auth-server-url}")
    private String authServerUrl;
    @Value("${keycloak.realm}")
    private String realm;
    @Value("${keycloak.resource}")
    private String clientId;
    @Value("${keycloak.credentials.secret}")
    private String clientSecret;

    @Bean
    public RequestInterceptor requestTokenBearerInterceptor() {
        return requestTemplate -> {
            String token = obtainAccessToken();
            requestTemplate.header(HttpHeaders.AUTHORIZATION, "Bearer " + token);
        };
    }

    private String obtainAccessToken() {
        RestTemplate restTemplate = new RestTemplate();

        MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
        formData.add("grant_type", "client_credentials");
        formData.add("client_id", clientId);
        formData.add("client_secret", clientSecret);

        ResponseEntity<JsonNode> response = restTemplate.postForEntity(tokenUri, formData, JsonNode.class);
        JsonNode responseBody = response.getBody();

        return responseBody != null ? responseBody.get("access_token").asText() : null;
    }
}

3.3 Update the ProductClient Interface:

Now, let’s update the ProductClient interface to incorporate the new authentication configuration:

@FeignClient(name = "${product-service.name}", url = "${products.url}", configuration = FeignConfig.class)
public interface ProductClient {
    @GetMapping("/{id}")
    ResponseEntity<Map<String, Object>> getProductById(@PathVariable("id") Long id);
}

With these configurations, the order microservice is now able to engage in secure communication with the product microservice through the Feign client. This configuration ensures that the order service seamlessly acquires access tokens from Keycloak, guaranteeing that authentication and authorization are maintained throughout the entire interaction.

4. Product Service Security Configuration

Now, we need to configure the security control for the product service. We want to restrict access to the path /api/products/{id} so that only authenticated and authorized entities, whether they are valid services with valid tokens, can access it.

@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
    return http
      .authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
          .requestMatchers(new AntPathRequestMatcher("/actuator/**")).permitAll()
          .requestMatchers(new AntPathRequestMatcher("/api/products/{id}", "GET"))
              .authenticated()
          .requestMatchers(new AntPathRequestMatcher("/api/products/**"))
              .hasAnyRole("developer", "admin")
          .anyRequest().authenticated())
      .oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer
          .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter)))
      .sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
      .cors(cors -> cors.configurationSource(corsConfigurationSource()))
      .build();
}

5. Testing using Postman

Firstly, start the product and order service, and add a new product to the system. Next, create an order associated with the product created.

image-15 Secure Microservices with Keycloak Client Credentials Flow

Attempt to access the order’s details.

image-16 Secure Microservices with Keycloak Client Credentials Flow

Now, let’s intentionally attempt to access these resources with an incorrect client secret and restart the order service.

image-17 Secure Microservices with Keycloak Client Credentials Flow

As expected, this results in a 401 Unauthorized response, demonstrating that the security configurations are correctly preventing unauthorized requests.

However, it’s worth noting that our current configuration doesn’t differentiate between services. This means any microservice with a valid access token can access the product details. In real-world scenarios with diverse microservices accessing multiple APIs, this can pose challenges.

To achieve a more granular level of access control, we can implement authority checks based on OAuth2 scopes, allowing for more fine-tuned security measures.

6. Client Scopes

Scopes are primarily used in the context of OAuth 2.0 and OpenID Connect (OIDC). Scopes define the specific access rights that a client application requests from the authorization server (Keycloak) on behalf of the user. They determine what data or actions the client application is allowed to access.

The client application (e.g. order microservices) needs to be aware of the scope name it requires, and it should include that scope when requesting an access token.

The authorization server verifies whether the client is allowed the requested scope. If granted, the server includes the requested scope in the issued access token. This means the access token will carry the information about the permissions granted to the client, and the resource server (e.g. product microservices) can use this information to make access control decisions.

6.1. Defining Scopes in Keycloak

Begin by navigating to the Client scopes section within Keycloak. Here, we can create a new scope, let’s call it view-products.

Now, go to the Clients section and select the fullstack-springboot-client. In the Client Scopes tab, utilize the Add client scope option to associate the view-products scope we’ve just defined.

Screenshot-2023-11-07-at-2.47.47-PM Secure Microservices with Keycloak Client Credentials Flow

6.2 Update Product Service Security Configuration

Update the product service security configuration to recognize and validate the new scope. This ensures that only authorized requests can access specific resources.

@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
    return http
        .authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
            .requestMatchers(new AntPathRequestMatcher("/actuator/**")).permitAll()
            .requestMatchers(new AntPathRequestMatcher("/api/products/{id}", "GET"))
                .hasAuthority("SCOPE_view-products")
            .requestMatchers(new AntPathRequestMatcher("/api/products/**"))
                .hasAnyRole("developer", "admin")
            .anyRequest().authenticated())
        .oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer
            .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter)))
        .sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .cors(cors -> cors.configurationSource(corsConfigurationSource()))
        .build();
}

6.3 Verify using Postman

When attempting to retrieve order details via the API, we should expect to receive an Forbidden error. This is because the required scope is missing.

image-18 Secure Microservices with Keycloak Client Credentials Flow

6.4 Integrate Scope in Feign Config

To complete the scope-based access check, it’s necessary to update the FeignConfig to include the newly defined scope.

@Value("${keycloak.scope}")
private String scope;

...

private String obtainAccessToken() {
    RestTemplate restTemplate = new RestTemplate();

    MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
    formData add("grant_type", "client_credentials");
    formData.add("client_id", clientId);
    formData.add("client_secret", clientSecret);
    formData.add("scope", scope);

    ResponseEntity<JsonNode> response = restTemplate.postForEntity(tokenUri, formData, JsonNode.class);
    JsonNode responseBody = response.getBody();

    return responseBody != null ? responseBody.get("access_token").asText() : null;
}

With these adjustments, we should now be able to successfully retrieve order details.

image-19 Secure Microservices with Keycloak Client Credentials Flow

Scopes are a powerful tool for fine-grained access control, ensuring that only authorized entities can access specific resources within the system.

7. Service-to-Service Role-based Authorization

In more complex scenarios, adopting a structured approach involves the use of service account roles. A service role refers to a specific type of role used for fine-grained authorization and access control within the Keycloak. Service roles are typically used for authorization within the application.

7.1. Create a New Client in Keycloak

Start by navigating to the FullStackApp realm in Keycloak and create a new client named order-services. Activate Client authentication and enable service account roles for this client.

image-20 Secure Microservices with Keycloak Client Credentials Flow

7.2. Define and Assign Roles

Within the Roles tab, add a new role named product-service-caller. This role represents the specific authorization needed for the order service to call the product service.

image-21 Secure Microservices with Keycloak Client Credentials Flow

Now, switch to the Service Account Roles tab. This section enables us to assign roles to the service account associated with the order-services client. Click on Assign Role, filter by client roles, select product-service-caller from the Available Roles list, and then click Assign.

image-22 Secure Microservices with Keycloak Client Credentials Flow

7.3. Verify Role Assignment Using Postman

For validation, use Postman to request a token for the order-services client.

image-23 Secure Microservices with Keycloak Client Credentials Flow

Decode the token using a platform like jwt.io. When decoded, the token should display the roles associated with the order service, including the product-service-caller role we’ve added.

image-24 Secure Microservices with Keycloak Client Credentials Flow

7.4 Update Product Service Security Configuration

Now, modify the application.properties of the order service to use the client ID and client secret of the new order-services client. In the FeignConfig, we can remove the scope, as it’s no longer necessary with this setup.

@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
    return http
        .authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
            .requestMatchers(new AntPathRequestMatcher("/actuator/**")).permitAll()
            .requestMatchers(new AntPathRequestMatcher("/api/products/{id}", "GET"))
                .hasAuthority("ROLE_product-service-caller")
            .requestMatchers(new AntPathRequestMatcher("/api/products/**"))
                .hasAnyRole("developer", "admin")
            .anyRequest().authenticated())
        .oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer
            .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter)))
        .sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .cors(cors -> cors.configurationSource(corsConfigurationSource()))
        .build();
}

7.5. Enhance Role Extraction Methods

In this step, we need to enhance the role extraction process by modifying the extractResourceRoles() method in JwtAuthConverter. This change ensures that roles are extracted from all resources, not just those under a specific resource ID.

private Collection<? extends GrantedAuthority> extractResourceRoles(Jwt jwt)  {
    Map<String, Object> resourceAccess = jwt.getClaim("resource_access");
    if (resourceAccess == null) {
        return Set.of();
    }
    Set<GrantedAuthority> authorities = new HashSet<>();

    // Iterate over each resource in resource_access and extract its roles
    for (Map.Entry<String, Object> entry : resourceAccess.entrySet()) {
        Map<String, Object> resource = (Map<String, Object>) entry.getValue();
        Collection<String> resourceRoles = (Collection<String>) resource.get("roles");
        if (resourceRoles != null) {
            authorities.addAll(
                resourceRoles.stream()
                    .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                    .collect(Collectors.toList())
            );
        }
    }

    return authorities;
}

7.6. Verify using Postman

With these configurations in place, we are now ready to test the setup. Attempt to access the order retrieval endpoint.

image-25 Secure Microservices with Keycloak Client Credentials Flow

We’ll notice that the extracted roles now include product-service-caller, signifying that the order service is equipped with the necessary authorization to interact with the product service.

image-26 Secure Microservices with Keycloak Client Credentials Flow

8. Conclusion

Throughout this post, we’ve explored how the client credentials flow is ideal for server-to-server authentication. We also introduced the concept of scopes and how they define the permissions associated with an access token. For a more structured approach, we demonstrated how to use service account roles. This allows for fine-grained control of permissions and authorization within the microservices architecture. Grab the full source code on GitHub.

Share this content:

Leave a Comment

Discover more from nnyw@tech

Subscribe now to keep reading and get access to the full archive.

Continue reading