14 Resource Protection How to Configure Authorization Process Based on Oauth2 Protocol

14 Resource Protection How to Configure Authorization Process Based on OAuth2 Protocol #

In the previous lesson, we learned how to build an OAuth2 authorization server and mastered the method of generating tokens. Today, our focus is on how to use tokens to achieve specific authorization for service access. In the daily development process, we need to control the permissions of different functionalities of each service to different granularity, and we hope that this control method is flexible enough to ensure that different services can dynamically adjust the permission control system according to business scenarios. At the same time, in a microservices architecture, we also need to consider how to propagate tokens effectively among multiple services, ensuring that the entire service access chain is authorized and managed. With the help of the Spring Security framework, it is easy to achieve these requirements. Let us now dive into the learning process.

Integrating OAuth2 Authorization Mechanism in Microservices #

In OAuth2 protocol, individual microservices act as resource servers. Spring Security framework provides a dedicated annotation, @EnableResourceServer, to enable this. By adding the @EnableResourceServer annotation in the Bootstrap class, we declare that all content in this service is protected resources. The code example is as follows:

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

Once we add the @EnableResourceServer annotation in the microservice, the service will validate each HTTP request to determine if the Token information is included in the Header section. If there is no Token information, the access will be restricted directly; if there is Token information, the service will validate the Token by accessing the OAuth2 server. Now the question arises, how does each microservice communicate with the OAuth2 server and obtain the verification result of the incoming Token?

To answer this question, we need to clarify that the purpose of passing the Token to the OAuth2 authorization server is to obtain the user and authorization information contained in that Token. Therefore, we must establish an interaction relationship between each microservice and the OAuth2 authorization server. We can achieve this by adding the configuration item security.oauth2.resource.userInfoUri in the configuration file, as shown below:

security:
  oauth2:
    resource:
      userInfoUri: http://localhost:8080/userinfo

Here, http://localhost:8080/userinfo points to a custom endpoint in the OAuth2 authorization server, which is implemented as follows:

@RequestMapping(value = "/userinfo", produces = "application/json")
public Map<String, Object> user(OAuth2Authentication user) {
    Map<String, Object> userInfo = new HashMap<>();
    userInfo.put("user", user.getUserAuthentication().getPrincipal());
    userInfo.put("authorities", AuthorityUtils.authorityListToSet(user.getUserAuthentication().getAuthorities()));
    return userInfo;
}

The function of this endpoint is to retrieve the user information for the accessible protected services. Here, we use the OAuth2Authentication class, which holds the user’s principal and authority information.

When accessing the /userinfo endpoint with Postman, we need to pass a valid Token. Let’s take the Token generated in the previous lesson, 0efa61be-32ab-4351-9dga-8ab668ababae, as an example. We add the “Authorization” request header to the HTTP request. Please note that because we are using a bearer-type Token, we need to add the prefix “Bearer” before the actual value of the access_token. Of course, we can also select the protocol type as OAuth 2.0 in the “Authorization” tab and enter the Access Token, which is equivalent to adding the request header information. The following image shows the details:

image.png

Figure: Diagram of making an HTTP request using Token

We will make subsequent HTTP requests in this way. The result of this request is as follows:

{
  "user": {
    "password": null,
    "username": "spring_user",
    "authorities": [
      {
        "authority": "ROLE_USER"
      }
    ],
    "accountNonExpired": true,
    "accountNonLocker": true,
    "credentialsNonExpired": true,
    "enabled": true
  },
  "authorities": [
    "ROLE_USER"
  ]
}

We know that the Token 0efa61be-32ab-4351-9dga-8ab668ababae is generated by the user “spring_user”. In the result, we can see the user’s username, password, and the roles the username has. These information are consistent with the user information we initialized in the previous lesson. We can also try using the user “spring_admin” to repeat the above process.

Embedding Access Authorization Control in Microservices #

In a microservices system, each microservice acts as an independent resource server. The granularity of protecting its own resources is not fixed, and the access control can be fine-grained according to requirements. In Spring Security, different levels of access control are abstracted into three granularities: user, role, and request method, as shown in the following figure:

image-2.png

Figure: Three granularities of access control: user, role, and request method

Based on the above diagram, we can combine these three granularities in various ways to form three levels: user, user + role, and user + role + request method. The scope of resources that these three levels can access gradually decreases. The user level means that any authenticated user can access various resources within the service. The user + role level requires the user to belong to one or more specific roles on top of the user level. The user + role + request method level has the highest requirements, allowing access restrictions for certain HTTP operations. Let’s discuss these three levels next.

Access Control at User Level #

Through the previous lesson, we are familiar with the method of customizing configuration information by extending various ConfigurerAdapter configuration adapter classes. For a resource server, there is also a ResourceServerConfigurerAdapter class, and our approach is the same, that is, to inherit this class and override its configure method, as shown below:

@Configuration
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.authorizeRequests()
            .anyRequest()
            .authenticated();
    }
}

We noticed that the parameter of this method is an HttpSecurity object, and the anyRequest().authenticated() method in the above configuration specifies that any request to this service needs to be authenticated. Therefore, when we use a regular HTTP request to access any URL in the user-service, we will receive an “unauthorized” 401 error message. The solution is to set the “Authorization” request header and pass a valid token in the HTTP request. You can practice by following the previous examples.

User + Role Level Access Control #

For resources with higher security requirements, we should not expose the access entry to all authenticated users, but instead limit access to certain roles. Depending on different business scenarios, we can determine which services involve core business processes, and only allow access to those services for administrators with the “ADMIN” role. To achieve this, we can simply use antMatchers() and hasRole() methods in HttpSecurity to specify the resources and roles we want to restrict. We can create a new ResourceServerConfiguration class and override its configure method, as shown below:

@Configuration
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.authorizeRequests()
                .antMatchers("/order/**").hasRole("ADMIN")
                .anyRequest().authenticated();
    }
}

As you can see, we use the Ant matchers introduced in the “Access Authorization: How to effectively configure the security access process?” lesson to implement authorization management. Now, if we access this service using a token with the “User” role, we will receive an “access_denied” error message. If we use the token generated from the previous lesson for a user with the “ADMIN” role, and then access the service again, we will get a normal response.

User + Role + Operation Level Access Control #

Furthermore, we can further control specific HTTP methods for a specific endpoint. For example, if we consider the risk of updating resources under the “user” endpoint of a microservice to be high, we can add the HttpMethod.PUT constraint in antMatchers() of HttpSecurity.

@Configuration
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.authorizeRequests()
                .antMatchers(HttpMethod.PUT, "/user/**").hasRole("ADMIN")
                .anyRequest().authenticated();
    }
}

Now, if we use a token generated from a user with the “USER” role and make a PUT request to the “/order/” endpoint, we will also receive an “access_denied” error message. However, if we try to access it with a token generated from a user with the “ADMIN” role, we will receive a normal response.

Propagating Tokens in Microservices #

We know that a microservices system involves calling multiple services and forms a chain. Because access to all services in the process requires access control, we need to ensure that the generated token can be propagated in the service call chain, as shown in the following diagram:

image-3.png

Token propagation in microservices

So, how do we achieve token propagation as shown in the diagram? Spring Security provides an OAuth2RestTemplate utility class to propagate tokens in HTTP requests. To create an OAuth2RestTemplate object in our business code, we can use the following example code:

@Bean
public OAuth2RestTemplate oauth2RestTemplate(OAuth2ClientContext oauth2ClientContext,
                                             OAuth2ProtectedResourceDetails details) {
    return new OAuth2RestTemplate(details, oauth2ClientContext);
}

As you can see, with the OAuth2ClientContext and OAuth2ProtectedResourceDetails provided, we can create an OAuth2RestTemplate. OAuth2RestTemplate stores the token retrieved from the HTTP request header to an OAuth2ClientContext object, and the OAuth2ClientContext controls the user’s request information within the session scope to ensure separation of state between different users. On the other hand, OAuth2RestTemplate depends on the OAuth2ProtectedResourceDetails class, which encapsulates the properties we introduced in the previous lesson, such as clientId, clientSecret, and scope.

Once the OAuth2RestTemplate is created, we can use it to access a remote service, as shown in the following code:

@Component
public class OrderServiceClient {

    @Autowired
    OAuth2RestTemplate restTemplate;

    public Order getOrderById(String orderId) {
        ResponseEntity<Order> result = restTemplate.exchange(
                "http://orderservice/order/{orderId}",
                HttpMethod.GET,
                null, Order.class, orderId);
        Order order = result.getBody();
        return order;
    }
}

Clearly, with this remote call approach, all we need to do is replace the original RestTemplate with the OAuth2RestTemplate. All the details about token propagation are fully encapsulated in each request.

Summary and Preview #

In this lesson, we focused on authorization for service access. Through today’s learning, we clarified the three levels of granularity for embedding access authorization control in microservices. Additionally, in a microservices system, as it involves interactions between multiple services, it is important to ensure the effective propagation of tokens between these services. We can easily achieve these requirements with the help of the utility classes provided by Spring Security.

The summary of this lesson is as follows: Resource Protection: How to configure the authorization process based on the OAuth2 protocol?

Finally, here’s a question for you to think about: Can you describe the three levels of authorization for service access and their corresponding implementation methods? Feel free to share your learning experience in the comments section.