Skip to content

Password Grant Type Client Server#

  • In the Keycloak Setup, we learned how to set up Keycloak authorization server for Password Grant Type . Now, in this section, we will continue to configure a Spring Boot client server which will connect to Keycloak authorization server to get token and call to Resource server automatically when the user call to client server to get the data.

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 user use Postman to call to the Spring Boot Client application then it will automatically call to the Keycloak authorization server with the user credentials (username and password) to get the access token and then call to the Spring Boot Resource Server application to get the data and return to the Postman.
  • So it will be different a little with the Client Credentials Grant Type because when the Spring Boot client server will use the user credentials which are users created in Realms of Keycloak authorization server instead of credentials of created clients.
  • For setting up Keycloak authorization server please view Keycloak Setup with Client Credentials Grant Type.
  • For Resource Server setup, you can view Client Credentials Resource Server, the resource server for Client Credential Grant Type, Password Grant Type and Authorization Code Grant Type are almost the same, so we can reuse it.

Client 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
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
 <dependencyManagement>
            <dependencies>
                    <dependency>
                            <groupId>org.springframework.cloud</groupId>
                            <artifactId>spring-cloud-dependencies</artifactId>
                            <version>2021.0.0</version>
                            <type>pom</type>
                            <scope>import</scope>
                    </dependency>
            </dependencies>
    </dependencyManagement>

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

            <!--lombok-->
            <dependency>
                    <groupId>org.projectlombok</groupId>
                    <artifactId>lombok</artifactId>
                    <version>1.18.22</version>
                    <scope>provided</scope>
            </dependency>

            <!--Spring cloud openfeign-->
            <dependency>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-starter-openfeign</artifactId>
                    <version>3.1.0</version>
            </dependency>

            <dependency>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
                    <version>3.1.0</version>
            </dependency>

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

            <!-- Spring Boot oauth2 client -->
            <dependency>
                    <groupId>org.springframework.security</groupId>
                    <artifactId>spring-security-oauth2-client</artifactId>
                    <version>5.6.0</version>
            </dependency>

    </dependencies>
  • Like dependencies that we used for creating Client Credentials Client Server. To make Spring Boot application become client server we need to apply spring-boot-starter-security and spring-security-oauth2-client dependencies and also spring-cloud-starter-openfeign to make the call with the access token from the client to the resource server, this is the service to service communication.

OAuth2 Feign Configuration#

  • So Let's create a Feign interceptor config in which we will try to get the access token and set it to the request header to call to the resource server.
OAuth2FeignConfig.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
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
63
64
65
66
package com.springboot.security.spring.security.oauth.client.server.password.grant.type.config;

import feign.RequestInterceptor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpHeaders;
import org.springframework.security.oauth2.client.*;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import reactor.core.publisher.Mono;

import java.util.HashMap;
import java.util.Map;

public class OAuth2FeignConfig {

    public static final String CLIENT_REGISTRATION_ID = "keycloak";

    @Value("${spring.security.oauth2.client.registration.keycloak.username}")
    private String username;

    @Value("${spring.security.oauth2.client.registration.keycloak.password}")
    private String password;

    private final ClientRegistrationRepository clientRegistrationRepository;
    private final OAuth2AuthorizedClientService authorizedClientService;

    public OAuth2FeignConfig(ClientRegistrationRepository clientRegistrationRepository,
                             OAuth2AuthorizedClientService authorizedClientService) {
        this.clientRegistrationRepository = clientRegistrationRepository;
        this.authorizedClientService = authorizedClientService;
    }

    @Bean
    public RequestInterceptor requestInterceptor() {
        OAuthClientCredentialsFeignManager clientCredentialsFeignManager =
                new OAuthClientCredentialsFeignManager(authorizedClientManager(), clientRegistrationRepository);
        return requestTemplate -> {
            requestTemplate.header(HttpHeaders.AUTHORIZATION, clientCredentialsFeignManager.getAccessToken(CLIENT_REGISTRATION_ID));
        };
    }

    @Bean
    public OAuth2AuthorizedClientManager authorizedClientManager() {
        OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
                .password()
                .refreshToken()
                .build();

        AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager =
                new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientService);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);


        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
        authorizedClientManager.setContextAttributesMapper(oAuth2AuthorizeRequest -> {
            Map<String, Object> map = new HashMap<>();
            map.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
            map.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
            return map;
        });
        return authorizedClientManager;
    }

}
  • So as you can see firstly, we will try to create and config the bean authorizedClientManager in the method authorizedClientManager();
  • To create and configure authorizedClientManager for the password grant type:
    • we have to create an OAuth2AuthorizedClientProvider with password grant and refreshToken to make sure our client will fetch new access token automatically when the old token is expired. Then we set OAuth2AuthorizedClientProvider to the authorizedClientManager.
    • Then we also need to set mapping attributes into the OAuth2AuthorizationContext which are the username/password of a Realm in Keycloak server. They will be used by the OAuth2AuthorizationContext to authorize (or re-authorize) the client identified by the provided clientRegistrationId.
  • The username and password of the user are load manually from the application.yml because the Password Grant Type is deprecated.
application.yml
 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
server:
  port: 8086

spring:
  security:
    oauth2:
      url: http://localhost:8087
      client:
        registration:
          keycloak: # <--- It's your custom client. I am using keycloak
            client-id: passwordgrant
            authorization-grant-type: password
            scope: openid, address, email, profile # your scopes
            username: user
            password: user
        provider:
          keycloak: # <--- Here Registered my custom provider
            authorization-uri: http://localhost:8080/auth/realms/myrealm/protocol/openid-connect/auth
            token-uri: http://localhost:8080/auth/realms/myrealm/protocol/openid-connect/token


logging:
  level:
    com.springboot.security.spring.security.oauth.client.server.password.grant.type.api: DEBUG

feign:
  client:
    config:
      default:
        loggerLevel: full
  • Next, after finishing OAuthClientCredentialsFeignManager config, then we will use it and clientRegistrationRepository to get the access token from the Keycloak authorization server. But implementing the function for getting access token we will put it into a class OAuthClientCredentialsFeignManager to make the code look better. So in the OAuthClientCredentialsFeignManager we will have a method as below.
OAuthClientCredentialsFeignManager.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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.springboot.security.spring.security.oauth.client.server.password.grant.type.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;

import static java.util.Objects.isNull;

@Slf4j
public class OAuthClientCredentialsFeignManager {

    private static final Authentication ANONYMOUS_USER_AUTHENTICATION =
            new AnonymousAuthenticationToken("key", "anonymous", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));

    private final OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager;
    private final ClientRegistrationRepository clientRegistrationRepository;


    public OAuthClientCredentialsFeignManager(OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager,
                                              ClientRegistrationRepository clientRegistrationRepository) {
        this.oAuth2AuthorizedClientManager = oAuth2AuthorizedClientManager;
        this.clientRegistrationRepository = clientRegistrationRepository;
    }

    public String getAccessToken(String clientRegistrationId) {
        try {
            ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId(clientRegistrationId);
            OAuth2AuthorizeRequest oAuth2AuthorizeRequest = OAuth2AuthorizeRequest
                    .withClientRegistrationId(clientRegistration.getRegistrationId())
                    .principal(ANONYMOUS_USER_AUTHENTICATION)
                    .build();
            OAuth2AuthorizedClient client = oAuth2AuthorizedClientManager.authorize(oAuth2AuthorizeRequest);
            if (isNull(client)) {
                throw new IllegalStateException("client credentials flow on " + clientRegistration.getRegistrationId() + " failed, client is null");
            }
            return "Bearer " + client.getAccessToken().getTokenValue();
        } catch (Exception exp) {
            log.error("client credentials error " + exp.getMessage());
            throw new IllegalArgumentException("client credentials error " + exp.getMessage(), exp);
        }
    }

}
  • As you can see, we will use ClientRegistrationRepository to get the the ClientRegistration and use to to build the OAuth2AuthorizeRequest. Then the OAuth2AuthorizeRequest will be used by oAuth2AuthorizedClientManager to authorize the client. After client is authorized then we can get the access token from OAuth2AuthorizedClient by function .getAccessToken().getTokenValue(); and this token will be used for Feign Interceptor.

Feign Configuration#

  • Next, we also need to create an adapter interface name SpringOAuth2ResourceClient to configure FeignClient with target api as below:
SpringOAuth2ResourceClient.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package com.springboot.security.spring.security.oauth.client.server.password.grant.type.api;

import com.springboot.security.spring.security.oauth.client.server.password.grant.type.config.OAuth2FeignConfig;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@FeignClient(name = "springOAuth2ResourceClient", url = "${spring.security.oauth2.url}", configuration = {OAuth2FeignConfig.class})
public interface SpringOAuth2ResourceClient {

    @RequestMapping(method = RequestMethod.GET, path = "/v1/card")
    String getCardDetail();

}
  • We will put the FeignClient configuration class that we created in the step above into this FeignClient by using param configuration. So now, every request of the api /v1/card to the Spring Boot resource server will have the access token which is fetched from the Keycloak authorization server.

  • Finally, in the main class, we just need to put annotation @EnableFeignClients as below.

SpringSecurityOauthPasswordGrantTypeClientServer.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package com.springboot.security.spring.security.oauth.client.server.password.grant.type;  

import org.springframework.boot.SpringApplication;  
import org.springframework.boot.autoconfigure.SpringBootApplication;  
import org.springframework.cloud.openfeign.EnableFeignClients;  

@EnableFeignClients  
@SpringBootApplication  
public class SpringSecurityOauthPasswordGrantTypeClientServer {  
    public static void main(String[] args) {  
        SpringApplication.run(SpringSecurityOauthPasswordGrantTypeClientServer.class, args);  
    }  
}

Controller#

  • Let's create a simple controller in the Spring Boot client application which will be called by the Postman as below.
OpenFeignInterceptorController.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package com.springboot.security.spring.security.oauth.client.server.password.grant.type.controller;

import com.springboot.security.spring.security.oauth.client.server.password.grant.type.api.SpringOAuth2ResourceClient;
import org.springframework.beans.factory.annotation.Autowired;
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 OpenFeignInterceptorController {

    @Autowired
    private SpringOAuth2ResourceClient springOAuth2ResourceClient;

    @RequestMapping(method = RequestMethod.GET, path = "/v1/oauth2/auth/password-grant-type/interceptor/card")
    public ResponseEntity<String> getCardMessage() {
        return ResponseEntity.ok(this.springOAuth2ResourceClient.getCardDetail());
    }

}
  • So when this api is called by postman then it will use the SpringOAuth2ResourceClient to call to the Spring Boot resource server to get the data.

Basic Security Configuration#

  • We will increase the security for our Spring Boot client service a little bit. In detail, to call the api in the controller of the step above, the user from Postman must put a basic authentication with username and password.
  • So let's create a SecurityConfig.java class and put some basic authentication configuration as below.
SecurityConfig.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
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
63
package com.springboot.security.spring.security.oauth.client.server.password.grant.type.config;

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.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Collections;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().cors().configurationSource(this.corsConfigurationSource())
                .and().csrf().disable()
                .authorizeRequests()
                .anyRequest()
                .authenticated()
                .and().formLogin()
                .and().httpBasic();
        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("AUTHORIZATION"));
        corsConfiguration.setMaxAge(3600L);
        source.registerCorsConfiguration("/**", corsConfiguration);
        return source;
    }

    @Bean
    public UserDetailsService inMemoryUserDetailsService() {
        UserDetails demo = User.withUsername("user").password("12345").roles("POSTMAN").build();
        return new InMemoryUserDetailsManager(demo);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

}

Note: The properties under spring.security.user won't work because Spring Boot will back off creating the UserDetailService bean. So, we will have to define the UserDetailsService bean by our self.

Testing#

  • Now, let's use Postman to call to api /v1/oauth2/auth/password-grant-type/interceptor/card of our Spring Boot Client server, then you can receive the data of Sping Boot Resource server successfully as below.

 #zoom

  • If you look into the our Spring Boot Client service then you can see the access token is added into the Authorization header of the request to the Spring Boot Resource server and if you make more calls from Postman, you can see the access token has not changed, so our Spring Boot Client server had reused the access token because it is still not expired.

 #zoom

  • Then if you wait a little bit and make a call from Postman again, then you can see in the log we will have a new access token. So it means the old access token has been expired so our Spring Boot Client server will call to Keycloak authorization server to get the access token again.

 #zoom

See Also#

References#