Skip to content

Client Credentials Resource Server#

  • In the Keycloak Setup, we learned how to set up Keycloak authorization server for Client Credentials Grant Type . Now, in this section, we will continue to configure a Spring Boot Token Signature Resource Server to verify in coming token with Keycloak before accessing the data.
  • If you haven't know about Token Signature Resource Server, you can view this post: OAUTH2 Resource Server Token Validation.

Prepare#

  • So firstly, we need to overview the simple diagram that we are going to do as in the image below.

 #zoom

  • So as you can see, when the Resource Server is starting up, then it will call to the Keycloak authorization server to get the public key.
  • Then if the Client calls to Keycloak authorization server to get the access token, the the Keycloak authorization server will generate and use it's private key to sign the access token and send back to client.
  • Then when the client call to Resource Server with the access token. So the Resource Server will use the public key which is downloaded from Keycloak authorization server in the start up time to verify the token and check it is valid of not to make sure the token is no tampered.

As the authorization server makes available new public keys, Spring Security will automatically rotate the public keys used to validate access tokens.

  • So with the Token Signature Resource Server. Whenever the Client sends the same access token to the Resource Server, it doesn't have to make a call to Auth Server or doesn't have to look into the database. It can simply check the signature or hash value of the token generated with the encryption algorithm that it maintains to understand the token is valid or not.

  • For setting up Keycloak authorization server please view Keycloak Setup with Client Credentials Grant Type.

Resource Server Setup#

Dependencies#

  • Now, let's create a Spring Boot application and add some dependencies as below.
pom.xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<dependencies>
        <!-- lombok -->
        <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.22</version>
                <scope>provided</scope>
        </dependency>

        <!-- Spring Boot Starter -->
        <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <version>2.6.3</version>
        </dependency>

        <!-- Spring security -->
        <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
                <version>2.6.3</version>
        </dependency>

        <!-- Spring Boot oauth2 resource server -->
        <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
                <version>2.6.3</version>
        </dependency>
</dependencies>
  • To make Spring Boot application become resource service we need to apply spring-boot-starter-security and spring-boot-starter-oauth2-resource-server dependencies.

Controller#

  • Let's create a simple controller for the Spring Boot Resource server which will be protected and need the access token which is issued from Keycloak authorization server to access.
CardController.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package com.springboot.sprint.security.oauth.client.credentials.grant.type.controller;  

import org.springframework.http.ResponseEntity;  
import org.springframework.web.bind.annotation.RequestMapping;  
import org.springframework.web.bind.annotation.RequestMethod;  
import org.springframework.web.bind.annotation.RestController;  

@RestController  
public class CardController {  

    @RequestMapping(method = RequestMethod.GET, path = "/v1/card")  
    public ResponseEntity<String> getCardDetail() {  
        return ResponseEntity.ok("This is your card details");  
    }  

}

Configuration#

  • Firstly, we need to create a KeycloakRoleConverter.java as below.
KeycloakRoleConverter.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.springboot.sprint.security.oauth.client.credentials.grant.type.config;

import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;

import java.util.*;
import java.util.stream.Collectors;

public class KeycloakRoleConverter implements Converter<Jwt, Collection<GrantedAuthority>> {

    @Override
    public Collection<GrantedAuthority> convert(Jwt jwt) {
        Map<String, Object> realmAccess = (Map<String, Object>) jwt.getClaims().get("realm_access");
        if (Objects.isNull(realmAccess) || realmAccess.isEmpty()) {
            return new ArrayList<>();
        }

        Collection<GrantedAuthority> returnValue = ((List<String>) realmAccess.get("roles"))
                .stream()
                .map("ROLE_"::concat) // prefix to map to a Spring Security "ROLE"
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
        return returnValue;
    }
}
  • This converter class is used to extract the roles from the jwt access token. The roles field is a part of the claim realm_access. Each role will be converted to a SimpleGrantedAuthority object.
  • Let's take a look into the access token that we got in the Keycloak Setup, then we can see where the roles field of access token is stored.

 #zoom

 #zoom

  • Now, let's continue to create the ProjectSecurityConfig.java class to configure the security for Resource Server apis as below.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package com.springboot.sprint.security.oauth.client.credentials.grant.type.config;  

import com.springboot.sprint.security.oauth.client.credentials.grant.type.constant.SecurityConstant;  
import org.springframework.context.annotation.Bean;  
import org.springframework.context.annotation.Configuration;  
import org.springframework.security.config.annotation.web.builders.HttpSecurity;  
import org.springframework.security.config.http.SessionCreationPolicy;  
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;  
import org.springframework.security.crypto.password.PasswordEncoder;  
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;  
import org.springframework.security.web.SecurityFilterChain;  
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;  
import org.springframework.web.cors.CorsConfiguration;  
import org.springframework.web.cors.CorsConfigurationSource;  
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;  

import java.util.Collections;  

@Configuration  
public class ProjectSecurityConfig {  

    @Bean  
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {  
        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();  
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new KeycloakRoleConverter());  
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)  
                .and().cors().configurationSource(this.corsConfigurationSource())  
                .and().csrf().ignoringAntMatchers("/v1/user")  
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())  
                .and().authorizeRequests()  
                .antMatchers("/v1/user").hasAnyRole("USER", "ADMIN")  
                .antMatchers("/v1/accounts/**").hasRole("USER")  
                .antMatchers("/v1/balance").hasRole("USER")  
                .antMatchers("/v1/loan").hasRole("ADMIN")  
                .antMatchers("/v1/card").hasRole("ADMIN")  
                .antMatchers("/v1/contact").permitAll()  
                .antMatchers("/v1/notice").permitAll()  
                .and().oauth2ResourceServer()  
                .jwt().jwtAuthenticationConverter(jwtAuthenticationConverter);  
        return http.build();  
    }  

    @Bean  
    protected CorsConfigurationSource corsConfigurationSource() {  
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();  
        CorsConfiguration corsConfiguration = new CorsConfiguration();  
        corsConfiguration.setAllowedHeaders(Collections.singletonList("*"));  
        corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*"));  
        corsConfiguration.setAllowedMethods(Collections.singletonList("*"));  
        corsConfiguration.setAllowCredentials(true);  
        corsConfiguration.setExposedHeaders(Collections.singletonList(SecurityConstant.AUTHORIZATION_HEADER));  
        corsConfiguration.setMaxAge(3600L);  
        source.registerCorsConfiguration("/**", corsConfiguration);  
        return source;  
    }  

    @Bean  
    public PasswordEncoder passwordEncoder() {  
        return new BCryptPasswordEncoder();  
    }  

}
  • So we will need to create a PasswordEncoder with BCryptPasswordEncoder because the access token is a jwt.
  • Next, Then for the api security configuration, we will use hasRole and hasAnyRole with the value is the role that we got from the access token (USER, ADMIN).
  • Finally the most important to make Spring Boot Resource Server security configuration works is the config below:

.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter);

  • In which we will config the jwt authentication conveter for the oauth2ResourceServer with the KeycloakRoleConverter that we created on the step above. So base on this configuration the our Resource Server can read the jwt access token and extract the roles correctly and compare with the roles that we configured in the ProjectSecurityConfig.java.

  • Finally, we need to put the configuration as below into the application.yml to make the Resource Server can know where to download the public key from the authorization server when the Resource Server starts up.

application.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
server:  
  port: 8085  

spring:  
  security:  
    oauth2:  
      resourceserver:  
        jwt:  
          jwk-set-uri: http://localhost:8080/auth/realms/myrealm/protocol/openid-connect/certs  

logging:  
  level:  
    org.springframework.security.web.FilterChainProxy: DEBUG
  • The Url for downloading the public key from the authorization server can be found in the discovery endpoint of Keycloak with the parttern as below:
{{KEY_CLOAK_DOMAIN}}/auth/realms/{{REALM_ID}}/protocol/openid-connect/certs  

Example: 

http://localhost:8080/auth/realms/myrealm/protocol/openid-connect/certs  

 #zoom

Testing#

  • Let's start the Keycloak authorization server first then start the Spring Boot Resource Server application later.

If the authorization server is down when Resource Server queries it (given appropriate timeouts), then startup will fail.

  • Then, we can start testing following steps below:
  • Firstly, we need to use Postman to call to the Keycloak authorization server to get the access token with the information that we created in the Keycloak Setup Client Credentials Grant Type as in the image below.

 #zoom

  • Then we will try to call to the /v1/card api that we created from the beginning, then you should received a successful result as below.

 #zoom

  • Now, if you wait a little bit about 5 minutes, the token will be expired or you can change a single character in the access token and you call again you will get the 401 error.

 #zoom

  • Because the api /v1/card need the role ADMIN to access, so if we go to the Keycloak authorization server and unassign the role ADMIN out of the myclient and get access token and try again, then you will get the 403 error as below.

 #zoom

 #zoom

 #zoom

  • It means the v1/card api require the access token with ADMIN role, but the access token only has USER role, so we will get 403 error code.

See Also#

References#