2 First steps in securing microservices

This chapter covers

  • Developing a microservice in Spring Boot/Java
  • Running and testing a Spring Boot/Java microservice with curl
  • Securing a microservice at the edge with OAuth 2.0
  • Enforcing authorization at the service level with OAuth 2.0 scopes

You build applications as a collection of smaller/modular services or components when you adhere to architectural principles of microservices. A system by itself, or a system on behalf of a human user or another system, can invoke a microservice. In all three cases, we need to properly authenticate and authorize all the requests that reach the microservice. A microservice may also consume one or more other microservices in order to cater to a request. In such cases, it is also necessary to propagate user context (from downstream services or client applications) to upstream microservices.

In this chapter, we explain how the security validation of the incoming requests happens, and in chapter 3, we discuss how to propagate the user context to upstream microservices. The focus of this chapter is to get you started with a straightforward deployment. The design of the samples presented in this chapter is far from a production deployment. As we proceed in the book, we explain how to fill the gaps and how to build a production-grade microservices security design step by step.

2.1 Building your first microservice

In this section, we discuss how to write, compile, and run your first microservice using Spring Boot. You will learn some basics about the Spring Boot framework and how you can use it to build microservices. Throughout this book, we use a retail store application as an example, which we build with a set of microservices. In this section, we build our first microservice, which accepts requests to create and manage orders, using Spring Boot (https://spring.io/projects/spring-boot).

Spring Boot is a framework based on the Spring platform that allows you to convert functions written in the Java programming language to network-accessible functions, known as services or APIs, by decorating your code with a special set of annotations. If you’re not familiar with Java, you still have nothing to worry about, because we don’t expect you to write code yourself. All the code samples you see in this book are available on GitHub (https://github.com/microservices-security-in-action/samples). As long as you are or have been a software developer, you’ll find it easy to understand the code.

Figure 2.1 shows a set of microservices, which are part of the retail store application we are building, with a set of consumer applications. The consumer applications, in fact, are the consumers of the microservices we build.


Figure 2.1 In this typical microservices deployment, consumer applications (a web app or a mobile app) access microservices on behalf of their end users, while microservices communicate with each other.

2.1.1 Downloading and installing the required software

To build and run samples we use in this chapter and throughout the rest of this book, you need to have a development environment set up with the Java Development Kit (JDK), Apache Maven, the curl command-line tool, and the Git command-line client.

Installing the JDK

The JDK is required to compile the source code in the samples. You can download the latest JDK from http://mng.bz/OMmo. We used Java version 11 to test all the samples.

Installing Apache Maven

Maven is a project management and comprehension tool that makes it easy to declare third-party (external) dependencies of your Java project required in the compile/build phase. It has various plugins such as the compiler plugin, which compiles your Java source code and produces the runnable artifact (binary). You can download Maven from the Apache website (https://maven.apache.org/download.cgi). Follow the installation instructions at https://maven.apache.org/install.html to install Maven on your operating system. We used Maven version 3.5 to test all the samples. To work with the samples in the book, we do not expect you to know Maven in detail, and where required, the book provides all the necessary commands. If you are interested in learning Maven, we recommend Mastering Apache Maven 3 (Packt Publishing, 2014) by Prabath Siriwardena, a coauthor of this book.

Installing curl

Download and install the curl command-line tool from the curl website (https://curl.haxx.se/download.html). You use curl in the book as a client application to access microservices. Most of the operating systems do have curl installed out of the box.

Installing the Git command-line tool

Download and install the Git command-line client on your computer, based on your operating system. You use the Git client only once to clone our samples Git repository. It’s not a must to install the Git client; you can also download the complete sample Git repository as a zip file from https://github.com/microservices-security-in-action/samples. When you click the Clone or Download button, you will find a link to download a zip file.

2.1.2 Clone samples repository

Once you complete the steps in section 2.1.1, and you’d like to clone the samples Git repository, rather than download it as a zip file, you can run the following command. Once successfully executed, it creates a directory called samples in your local filesystem, with all the samples we have for the book:

> git clone 
https://github.com/microservices-security-in-action/samples.git

2.1.3 Compiling the Order Processing microservice

Once you complete the preceding steps, it’s time to get your hands dirty and run your first microservice. First, open the command-line tool in your operating system, and navigate to the location on your filesystem where you cloned the samples repository; in the rest of the book, we identify this location as [samples]:

> cd [samples]/chapter02/sample01

Inside the chapter02/sample01 directory, you’ll find the source code corresponding to the Order Processing microservice. From within that directory, execute the following command to build the Order Processing microservice:

> mvn clean install

If you run into problems while running the command, old and incompatible dependencies might reside in your local Maven repository. To get rid of such problems, try removing (or renaming) the .m2 directory that resides in your home directory (~/.m2/). The preceding command instructs Maven to compile your source code and produce a runnable artifact known as a Java Archive (JAR) file. Note that you need to have Java and Maven installed to execute this step successfully. If your build is successful, you’ll see the message BUILD SUCCESS.

If this is the first time you’re using Maven to build a Spring Boot project, Maven downloads all the Spring Boot dependencies from their respective repositories; therefore, an internet connection is required in the build phase. The first-time build is expected to take slightly longer than the next attempts. After the first build, Maven installs all the dependencies in your local filesystem, and that takes the build times down considerably for the subsequent attempts.

If the build is successful, you should see a directory named target within your current directory. The target directory should contain a file named com.manning .mss.ch02.sample01-1.0.jar. (Other files will be within the target directory, but you’re not interested in them at the moment.) Then run the following command from the chapter02/sample01/ directory to spin up the Order Processing microservice. Here, we use a Maven plugin called spring-boot:

> mvn spring-boot:run

If the microservice started successfully, you should see a bunch of messages being printed on the terminal. At the bottom of the message stack, you should see this message:

Started OrderApplication in <X> seconds

By default, Spring Boot starts the microservice on HTTP port 8080. If you have any other services running on your local machine on port 8080, make sure to stop them; alternatively, you can change the default port of the Order Processing microservice by changing the value of the server.port property as appropriate in the chapter02/sample01/src/main/resources/application.properties file. But, then again, it would be much easier to follow the rest of the samples in the chapter, with minimal changes, if you keep the Order Processing microservice running on the default port.

2.1.4 Accessing the Order Processing microservice

By default, Spring Boot runs an embedded Apache Tomcat web server that listens for HTTP requests on port 8080. In this section, you access your microservice using curl as the client application. In case you run the Order Processing microservice on a custom port, make sure to replace the value of the port (8080) in the following command with the one you used. To invoke the microservice, open your command-line client and execute the following curl command:

> curl  -v http://localhost:8080/orders 

-H 'Content-Type: application/json' 
--data-binary @- << EOF
{
  "items":[
    {
      "itemCode":"IT0001",
      "quantity":3
    },
    {
      "itemCode":"IT0004",
      "quantity":1
    }
  ],
  "shippingAddress":"No 4, Castro Street, Mountain View, CA, USA"
}
EOF

You should see this message on your terminal:

{

  "orderId":"1633c9bd-7b9b-455f-965e-91d41331063c",
  "items":[
    {
      "itemCode":"IT0001",
      "quantity":3
    },
    {
      "itemCode":"IT0004",
      "quantity":1
    }
  ],
  "shippingAddress":"No 4, Castro Street, Mountain View, CA, USA"
}

If you see this message, you’ve successfully developed, deployed, and tested your first microservice!

NOTE All samples in this chapter use HTTP (not HTTPS) endpoints to spare you from having to set up proper certificates and to make it possible for you to inspect messages being passed on the wire (network), if required. In production systems, we do not recommend using HTTP for any endpoint. You should expose all the endpoints only over HTTPS. In chapter 6, we discuss how to secure microservices with HTTPS.

When you executed the preceding command, curl initiated an HTTP POST request to the /orders resource located on the server localhost on port 8080 (local machine). The content (payload) of the request represents an order placed for two items to be shipped to a particular address. The Spring Boot server runtime (embedded Tomcat) dispatched this request to the placeOrder function (in the Java code) of your Order Processing microservice, which responded with the message.

2.1.5 What is inside the source code directory?

Let’s navigate inside the sample01 directory and inspect its contents. You should see a file named pom.xml and a directory named src. Navigate to the src/main/java/com/manning/mss/ch02/sample01/service/ directory. You’ll see two files: OrderApplication.java and OrderProcesingService.java.

Before you dig into the contents of these files, let us explain what you’re trying to build here. As you’ll recall, a microservice is a collection of network-accessible functions. In this context, network-accessible means that these functions are accessible over HTTP (https://tools.ietf.org/html/rfc2616) through applications such as web browsers and mobile applications, or software such as curl (https://curl.haxx.se/) that’s capable of communicating over HTTP. Typically, a function in a microservice is exposed as an action over a REST resource (https://spring.io/guides/tutorials/rest/). Often, a resource represents an object or entity that you intend to inspect or manipulate. When mapped to HTTP, a resource is usually identified by a request URI, and an action is represented by an HTTP method; see sections 5.1.1 and 5.1.2 of the HTTP specification or RFC 2616 (https://tools.ietf.org/html/rfc2616#page-35).

Consider a scenario in which an e-commerce application uses a microservice to retrieve the details of an order. An HTTP request template that maps to that particular function in the microservice looks similar to the following:

GET /orders/{orderid}

GET is the HTTP method used in this case, since you’re performing a data-retrieval operation. /orders/{orderid} is the resource path on the server that hosts the corresponding microservice. This path can be used to uniquely identify an order resource. {orderid} is a variable that needs to be replaced with proper values in the actual HTTP request. Something like GET /orders/d59dbd56-6e8b-4e06-906f-59990ce2e330 would ask the microservice to retrieve details of the order with ID d59dbd56-6e8b-4e06-906f-59990ce2e330.

2.1.6 Understanding the source code of the microservice

Now that you have a fair understanding of how to expose a microservice as an HTTP resource, let’s look at the code samples to see how to develop a function in Java and use Spring Boot to expose it as an HTTP resource. Use the file browser in your operating system to open the directory located at sample01/src/main/java/com/manning/mss/ch02/sample01/service, and open the OrderProcessingService.java file in a text editor. If you’re familiar with Java integrated development environments (IDEs) such as Eclipse, NetBeans, IntelliJ IDEA, or anything similar, you can import the sample as a Maven project to the IDE. The following listing shows what the content of the OrderProcessingService.java file looks like.

Listing 2.1 The content of the OrderProcessingService.java file

@RestController                                              
@RequestMapping("/orders")                                   
public class OrderProcessingService {

    private Map<String, Order> orders = new HashMap<>();

    @PostMapping                                             
    public ResponseEntity<Order> placeOrder(@RequestBody Order order) {

        System.out.println("Received Order For " 
                           + order.getItems().size() + " Items");
        order.getItems().forEach((lineItem) -> 
        System.out.println("Item: " + lineItem.getItemCode() +
                           " Quantity: " + lineItem.getQuantity()));

        String orderId = UUID.randomUUID().toString();
        order.setOrderId(orderId);
        orders.put(orderId, order);
}

Informs the Spring Boot runtime that you’re interested in exposing this class as a microservice

Specifies the path under which all the resources of the service exist

Informs the Spring Boot runtime to expose this function as a POST HTTP method

This code is a simple Java class with a function named placeOrder. As you may notice, we decorated the class with the @RestController annotation to inform the Spring Boot runtime that you’re interested in exposing this class as a microservice. The @RequestMapping annotation specifies the path under which all the resources of the service exist. We also decorated the placeOrder function with the @PostMapping annotation, which informs the Spring Boot runtime to expose this function as a POST HTTP method (action) on the /orders context. The @RequestBody annotation says that the payload in the HTTP request is to be assigned to an object of type Order.

Another file within the same directory is named OrderApplication.java. Open this file with your text editor and inspect its content, which looks like the following:

@SpringBootApplication

public class OrderApplication {
   public static void main(String args[]) {
      SpringApplication.run(OrderApplication.class, args);
   }
}

This simple Java class has only the main function. The @SpringBootApplication annotation informs the Spring Boot runtime that this application is a Spring Boot application. It also makes the runtime check for Controller classes (such as the OrderProcessingService class you saw earlier) within the same package of the OrderApplication class. The main function is the function invoked by the JVM when you command it to run the particular Java program. Within the main function, start the Spring Boot application through the run utility function of the SpringApplication class, which resides within the Spring framework.

2.2 Setting up an OAuth 2.0 server

Now that you have your first microservice up and running, we can start getting to the main focus of this book: securing microservices. You’ll be using OAuth 2.0 to secure your microservice at the edge.

If you are unfamiliar with OAuth 2.0, we recommend you first go through appendix A, which provides a comprehensive overview of the OAuth 2.0 protocol and how it works. In chapter 3, we discuss in detail why we opted for OAuth2.0 over options such as basic authentication and certificate-based authentication. For now, know that OAuth 2.0 is a clean mechanism for solving the problems related to providing your username and password to an application that you don’t trust to access your data.

When combined with JWT, OAuth2.0 can be a highly scalable authentication and authorization mechanism, which is critical when it comes to securing microservices.1 Those who know about OAuth 2.0 probably are raising their eyebrows at seeing it mentioned as a way of authentication. We agree that it’s not an authentication protocol at the client application end, but at the resource server end, which is the microservice.

2.2.1 The interactions with an authorization server

In an OAuth 2.0 flow, the client application, the end user, and the resource server all interact directly with the authorization server, in different phases (see figure 2.2). Before requesting a token from an authorization server, the client applications have to register themselves with it.


Figure 2.2 Actors in an OAuth2.0 flow: in a typical access delegation flow, a client--on behalf of the end user--accesses a resource that is hosted on a resource server by using a token provided by the authorization server.

An authorization server issues tokens only for the client applications it knows. Some authorization servers support Dynamic Client Registration Protocol (https://tools.ietf.org/html/rfc7591), which allows clients to register themselves on the authorization server on the fly or on demand (see figure 2.3).


Figure 2.3 A client application is requesting an access token from the authorization server. The authorization server issues tokens to only known client applications. A client application must register at the authorization server first.

The Order Processing microservice, which plays the role of the resource server here, would receive the token issued by the authorization server from the client, usually as an HTTP header or as a query parameter when the client makes an HTTP request (see step 1 in figure 2.4). It’s recommended that the client communicate with the microservice over HTTPS and send the token in an HTTP header instead of a query parameter. Because query parameters are sent in the URL, those can be recorded in server logs. Hence, anyone who has access to the logs can see this information.

Having TLS to secure the communication (or in other words, the use of HTTPS) between all the entities in an OAuth 2.0 flow is extremely important. The token (access token) that the authorization server issues to access a microservice (or a resource) must be protected like a password. We do not send passwords over plain HTTP and always use HTTPS. Hence we follow the same process when sending access tokens over the wire.


Figure 2.4 A client application is passing the OAuth access token in the HTTP Authorization header to access a resource from the resource server.

Upon receipt of the access token, the Order Processing microservice should validate it against the authorization server before granting access to its resources. An OAuth 2.0 authorization server usually supports the OAuth 2.0 token introspection profile (https://tools.ietf.org/html/rfc7662)or a similar alternative for resource servers to check the validity of an access token (see figure 2.5). If the access token is a self-contained JWT, the resource server can validate it, by itself, without talking to the authorization server. We discuss self-contained JWT in detail in chapter 6.


Figure 2.5 The Order Processing microservice (resource server) introspects the access token by talking to the authorization server.

2.2.2 Running the OAuth 2.0 authorization server

Many production-grade OAuth 2.0 authorization servers are out there, both proprietary and open source. However, in this chapter, we use a simple authorization server that’s capable of issuing access tokens. It is built using Spring Boot. Within the Git repository you cloned earlier, you should find a directory named sample02 under the directory chapter02. There you’ll find the source code of the simple OAuth 2.0 authorization server. First, compile and run it; then look into the code to understand what it does.

To compile, use your command-line client to navigate into the chapter02/sample02 directory. From within that directory, execute the following Maven command to compile and build the runnable artifact:

> mvn clean install

If your build is successful, you’ll see the message BUILD SUCCESS. You should find a file named com.manning.mss.ch02.sample02-1.0.jar within a directory named target. Execute the following command from within the chapter02/sample02 directory, using your command-line client, to run the OAuth 2.0 authorization server:

> mvn spring-boot:run

If you managed to run the server successfully, you should see this message:

Started OAuthServerApplication in <X> seconds

This message indicates that you successfully started the authorization server. By default, the OAuth 2.0 authorization server runs on HTTP port 8085. If you have any other services running on your local machine, on port 8085, make sure to stop them; alternatively, you can change the default port of the authorization server by changing the value of the server.port property as appropriate in the chapter02/sample02/src/main/resources/application.properties file. But, then again, it would be much easier to follow the rest of the samples in the chapter, with minimal changes, if you keep the authorization server running on the default port.

NOTE The OAuth 2.0 authorization server used in this chapter is running on HTTP, while in a production deployment it must be over HTTPS. In chapter 6, we discuss how to set up an authorization server over HTTPS.

2.2.3 Getting an access token from the OAuth 2.0 authorization server

To get an access token from the authorization server, use an HTTP client to make an HTTP request to the server. In the real world, the client application that is accessing the microservice would make this request. You’ll be using curl for this purpose as the HTTP client.To request an access token from the authorization server (which runs on port 8085), run the following command, using your command-line client:

> curl -u orderprocessingapp:orderprocessingappsecret 

-H "Content-Type: application/json" 
-d '{"grant_type": "client_credentials", "scope": "read write}' 
http://localhost:8085/oauth/token

Take a quick look at this request and try to understand it. You can think of orderprocessingapp:orderprocessingappsecret as the client application’s username (orderprocessingapp) and password (orderprocessingappsecret). The only difference is that these credentials belong to an application, not a user. The application being used to request a token needs to bear a unique identifier and a secret that’s known by the authorization server. The -u flag provided to curl instructs it to create a basic authentication header and send it to the authorization server as part of the HTTP request. Then curl base64-encodes the orderprocessingapp :orderprocessingappsecret string and creates the Basic authentication HTTP header as follows:

Authorization: Basic
b3JkZXJwcm9jZXNzaW5nYXBwOm9yZGVycHJvY2Vzc2luZ2FwcHNlY3JldA==

The string that follows the Basic keyword is the base64-encoded value of orderprocessingapp:orderprocessingappsecret. As you may have noticed, you’re sending a Basic authentication header to the token endpoint of the OAuth2.0 authorization server because the token endpoint is protected with basic authentication (https://tools.ietf.org/html/rfc2617). Because the client application is requesting a token here, the Basic authentication header should consist of the credentials of the client application, not of a user. Note that basic authentication here isn’t used for securing the resource server (or the microservice); you use OAuth 2.0 for that purpose. Basic authentication at this point is used only for obtaining the OAuth token required to access the microservice, from the authorization server.

In chapter 3, we discuss in detail why we chose OAuth 2.0 over protocols such as basic authentication and mTLS to secure your resource server. Even for securing the token endpoint of the OAuth 2.0 authorization server, instead of basic authentication, you can pick whichever authentication mechanism you prefer. For strong authentication, many prefer using certificates.

The parameter -H "Content-Type: application/json" in the preceding token request informs the authorization server that the client will be sending a request in JSON format. What follows the -d flag is the actual JSON content of the message, which goes in the HTTP body. In the JSON message, the grant_type specifies the protocol to be followed in issuing the token. We talk more about OAuth 2.0 grant types in chapter 3. For now, think of a grant type as the sequence of steps that the client application and the authorization server follow to issue an access token. In the case of the client_credentials grant type, the authorization server validates the Basic authentication header and issues an access token if it’s valid.

The scope declares what actions the application intends to perform with a token. When issuing a token, the authorization server validates whether the requesting application is permitted to obtain the requested scopes and binds them to the token as appropriate. If the application identified by orderprocessingapp can perform only read operations, for example, the authorization server issues the corresponding token under the scope read. The URL http://localhost:8085/oauth/token is the endpoint of the authorization server that issues access tokens. Your curl client sends the HTTP request to this endpoint to obtain an access token. If your request is successful, you should see a response similar to this:

{

  "access_token":"8c017bb5-f6fd-4654-88c7-c26ccca54bdd",
  "token_type":"bearer",
  "expires_in":300,
  "scope":"read write"
}

2.2.4 Understanding the access token response

The following list provides details on the preceding JSON response from the authorization server. If you are new to OAuth 2.0, please check appendix A for further details.

  • access_token--The value of the token issued by the authorization server to the client application (curl, in this case).

  • token_type--The token type (more about this topic when we talk about OAuth 2.0 in appendix A). Most of the OAuth deployments we see today use bearer tokens.

  • expires_in--The period of validity of the token, in seconds. The token will be considered invalid (expired) after this period.

  • scope--The actions that the token is permitted to perform on the resource server (microservice).

2.3 Securing a microservice with OAuth 2.0

So far, you’ve learned how to develop your first microservice and how to set up an OAuth 2.0 authorization server to get an access token. In this section, you’ll see how to secure the microservice you developed. Up to now, you’ve accessed it without any security in place.

2.3.1 Security based on OAuth 2.0

Once secured with OAuth 2.0, the Order Processing microservice now expects a valid security token (access token) from the calling client application. Then it will validate this access token with the assistance of the authorization server before it grants access to its resources. Figure 2.6 illustrates this scenario.


Figure 2.6 A client application accessing a secured microservice with an access token obtained from the authorization server. The Order Processing microservice talks to the authorization server to validate the token before granting access to its resources.

Here’s what happens in each of the steps illustrated in figure 2.6:

  1. The client application requests an OAuth2.0 access token from the authorization server.

  2. In response to the request in step 1, the authorization server issues an access token to the client application.

  3. The client application makes an HTTP request to the Order Processing microservice. This request carries the access token obtained in step 2 as an HTTP header.

  4. The Order Processing microservice checks with the authorization server to see whether the received access token is valid.

  5. In response to the request in step 4, the authorization server checks to see whether the provided access token is an active token in the system (its state is active) and whether the token is valid for that particular moment (it isn’t expired). Then it responds to the Order Processing microservice, indicating whether the access token is valid.

  6. In response to the request in step 3, and based on the result in step 5, the Order Processing microservice responds to the client application, either granting access to the resource being requested or sending an error message.

In the examples in this chapter so far, you’ve used the client_credentials grant type to obtain an access token from the authorization server. In this particular case, the token endpoint of the authorization server is protected via basic authentication with the client ID and the client secret of the application. The client_credentials grant type is good when the client application doesn’t need to worry about end users. If it has to, it should pick an appropriate grant type. The client_credentials grant type is used mainly for system-to-system authentication.

2.3.2 Running the sample

If you’re still running the Order Processing microservice from section 2.1, stop it, because you’re about to start a secured version of the same microservice on the same port. You can stop the microservice by going to the terminal window that is running it and pressing Ctrl-C. To run this sample, navigate to the directory where you cloned the samples from the Git repository from your command-line application, and go to the chapter02/sample03 directory. From within that directory, execute the following Maven command to build the sample:

> mvn clean install

If the build is successful, you should see a directory named target within your current directory. The target directory should contain a file named com.manning.mss.ch02 .sample03-1.0.jar. (Other files will be within the target directory, but you’re not interested in them at the moment.) Then run the following command from the chapter02/sample03/ directory to spin up the secured Order Processing microservice. Here, we use a Maven plugin called spring-boot:

> mvn spring-boot:run

If you managed to run the server successfully, you should see a message like this:

Started OrderApplication in <X> seconds

Now run the same curl command you used earlier in this chapter to access the Order Processing microservice:

> curl  -v http://localhost:8080/orders 

-H 'Content-Type: application/json' 
--data-binary @- << EOF
{
  "items":[
    {
      "itemCode":"IT0001",
      "quantity":3
    },
    {
      "itemCode":"IT0004",
      "quantity":1
    }
  ],
  "shippingAddress":"No 4, Castro Street, Mountain View, CA, USA"
}
EOF

You should see an error message saying that the request was unsuccessful. The expected response message is as follows:

{

 "error":"unauthorized", "error_description":"Full authentication is 
                       required to access this resource"
}

Your Order Processing microservice is now secured and can no longer be accessed without a valid access token obtained from the authorization server. To understand how this happened, look at the modified source code of your Order Processing microservice. Using your favorite text editor or IDE, open the file OrderProcessingService.java located inside the src/main/java/com/manning/mss/ch02/sample03/service directory. This is more or less the same class file you inspected earlier with a function named placeOrder. One addition to this class is the annotation @EnableWebSecurity. This annotation informs your Spring Boot runtime to apply security to the resources of this microservice. Following is the class definition:

@EnableResourceServer

@EnableWebSecurity
@RestController
@RequestMapping("/orders")
public class OrderProcessingService extends WebSecurityConfigurerAdapter {
}

If you further inspect this class, you should notice a method named tokenServices that returns an object of type ResourceServerTokenServices (see listing 2.2). Properties set in the RemoteTokenServices object (which is of the ResourceServerTokenServices type) are the ones that the Spring Boot runtime uses to communicate with the authorization server to validate credentials received by the Order Processing microservice (the resource server, in this case).

If you go through the code of the tokenServices function, you’ll see that it uses a method named setCheckTokenEndpointUrl to set the value http://localhost:8085/oauth/check_token as the TokenEndpointURL property in the RemoteTokenServices class. The TokenEndpointURL property is used by the Spring Boot runtime to figure out the URL on the OAuth 2.0 authorization server that it has to talk to, to validate any tokens it receives via HTTP requests. This is the URL the Order Processing microservice uses in step 4 of figure 2.6 to talk to the authorization server.

Listing 2.2 The tokenServices method from OrderProcessingService.java

@Bean
public ResourceServerTokenServices tokenServices() {
    RemoteTokenServices tokenServices = new RemoteTokenServices();
    tokenServices.setClientId("orderprocessingservice");
    tokenServices.setClientSecret("orderprocessingservicesecret");
    tokenServices
      .setCheckTokenEndpointUrl("http://localhost:8085/oauth/check_token");
    return tokenServices;
}

The endpoint that does the validation of the token itself is secure; it requires a valid Basic authentication header. This header should consist of a valid client ID and a client secret. In this case, one valid client ID and client secret pair is orderprocessingservice and orderprocessingservicesecret, which is why those values are set in the RemoteTokenServices object. In fact, these credentials are hardcoded in the simple OAuth server we developed.

In section 2.4, you’ll see how to use the token you obtained from the authorization server in section 2.2 to make a request to the now-secure Order Processing microservice.

2.4 Invoking a secured microservice from a client application

Before a client application can access your secured Order Processing microservice, it should obtain an OAuth2.0 access token from the authorization server. As explained in section 2.2.4, the client application at minimum requires a valid client ID and a client secret to obtain this token. The client ID and client secret registered on your OAuth 2.0 authorization server at the moment are orderprocessingapp and orderprocessingappsecret, respectively. As before, you can use the following curl command to obtain an access token:

> curl -u orderprocessingapp:orderprocessingappsecret 

-H "Content-Type: application/json" 
-d '{ "grant_type": "client_credentials", "scope": "read write" }' 
http://localhost:8085/oauth/token

If the request is successful, you should get an access token in response, as follows:

{

  "access_token":"8c017bb5-f6fd-4654-88c7-c26ccca54bdd",
  "token_type":"bearer",
  "expires_in":300,
  "scope":"read write"
}

As discussed earlier, 8c017bb5-f6fd-4654-88c7-c26ccca54bdd is the value of the access token you got, and it’s valid for 5 minutes (300 seconds). This access token needs to be provided to the HTTP request you’ll make to the Order Processing microservice. You need to send the token as an HTTP header named Authorization, and the header value needs to be prefixed by the string Bearer, as follows:

Authorization: Bearer 8c017bb5-f6fd-4654-88c7-c26ccca54bdd

The new curl command to access the Order Processing microservice is as follows:

> curl  -v http://localhost:8080/orders 

-H 'Content-Type: application/json' 
-H "Authorization: Bearer 8c017bb5-f6fd-4654-88c7-c26ccca54bdd" 
--data-binary @- << EOF
{
  "items":[
    {
      "itemCode":"IT0001",
      "quantity":3
    },
    {
      "itemCode":"IT0004",
      "quantity":1
    }
  ],
  "shippingAddress":"No 4, Castro Street, Mountain View, CA, USA"
}
EOF

Note that the -H parameter is used to pass the access token as an HTTP header named Authorization. This time, you should see the Order Processing microservice responding with a proper message saying that the order was successful:

{

  "orderId":"d59dbd56-6e8b-4e06-906f-59990ce2e330",
  "items":[
    {
      "itemCode":"IT0001",
      "quantity":3
    },
    {
      "itemCode":"IT0004",
      "quantity":1
    }
  ],
  "shippingAddress":"No 4, Castro Street, Mountain View, CA, USA"
}

If you see this message, you’ve successfully created, deployed, and tested a secured microservice. Congratulations! The access token that the client application (curl) sent in the HTTP header to the Order Processing microservice was validated against the authorization server. This process is called token introspection. Because the result of the introspection operation ended up being a success, the Order Processing microservice granted access to its resources.

2.5 Performing service-level authorization with OAuth 2.0 scopes

You need a valid access token to access a microservice. Authentication is the first level of defense applied to a microservice to protect it from spoofing. The authentication step that occurs before granting access to the microservice ensures that the calling entity is a valid client (user, application, or both) in the system. Authentication, however, doesn’t mention anything about the level of privileges the client has in the system.

A given microservice may have more than one operation. The Order Processing microservice, for example, has one operation for creating orders (POST /orders) and another operation for retrieving order details (GET /orders/{id}). Each operation in a microservice may require a different level of privilege for access.

A privilege describes the actions you’re permitted to perform on a resource. More often than not, your role or roles in an organization describe which actions you’re permitted to perform within that organization and which actions you’re not permitted to perform. A privilege may also indicate status or credibility. If you’ve traveled on a commercial airline, you’re likely familiar with the membership status of travelers who belong to airline frequent-flyer programs. Likewise, a privilege is an indication of the level of access that a user or an application possesses in a system.

In the world of OAuth 2.0, privilege is mapped to a scope. A scope is way of abstracting a privilege. A privilege can be a user’s role, membership status, credibility, or something else. It can also be a combination of a few such attributes. You use scopes to abstract the implication of a privilege. A scope declares the privilege required by a calling client application to grant access to a resource. The placeOrder operation, for example, requires a scope called write, and the getOrder operation requires a scope called read. The implications of write and read--whether they’re related to user roles, credibility, or anything else--is orthogonal from a resource-server point of view.

2.5.1 Obtaining a scoped access token from the authorization server

The authorization server you built in this chapter contains two applications: one with client ID orderprocessingapp, which you used for accessing the microservice, and one with client ID orderprocessingservice. You configured these applications in such a way that the first application, with client ID orderprocessingapp, has privileges to obtain both scopes read and write, whereas the second application, with client ID orderprocessingservice, has privileges to obtain only scope read, as explained in the following listing:

Listing 2.3 The configure method in OAuthServerConfig.java from sample02

clients.inMemory()
    .withClient("orderprocessingapp").secret("orderprocessingsecret")
    .authorizedGrantTypes("client_credentials", "password")
    .scopes("read", "write")
    .accessTokenValiditySeconds(3600)
    .resourceIds("sample-oauth")
    .and()
    .withClient("orderprocessingservice")
    .secret("orderprocessingservicesecret") 
    .authorizedGrantTypes("client_credentials", "password")
    .scopes("read")
    .resourceIds("sample-oauth");

This code indicates that anyone who uses orderprocessingapp is allowed to obtain an access token under both scopes read and write, whereas any user of orderprocessingservice is allowed to obtain an access token only under scope read. In all the requests so far to obtain an access token, you used orderprocessingapp as the client ID and requested both scopes read and write.

Now execute the same request to obtain an access token with orderprocessingservice as the client ID to see what the token response looks like. Execute this curl command to make the token request:

> curl -u orderprocessingservice:orderprocessingservicesecret 

-H "Content-Type: application/json" 
-d '{ "grant_type": "client_credentials", "scopes": "read write" }' 
http://localhost:8085/oauth/token

If the token request was successful, you should see this response:

{

  "access_token":"47190af1-624c-48a6-988d-f4319d36b7f4",
  "token_type":"bearer",
  "expires_in":3599,
  "scope":"read"
}

Notice that although in the token request you requested both scopes read and write, the OAuth 2.0 authorization server issued a token with scope read only. One good thing about the OAuth 2.0 authorization server is that although you may not have the privileges to get all the scopes you request, instead of refusing to issue an access token, the server issues an access token bound to the scopes that you’re entitled to. Then again, this may vary based on the authorization server you pick--and the OAuth 2.0 standard does not mandate a way that the authorization servers should handle such cases.

2.5.2 Protecting access to a microservice with OAuth 2.0 scopes

Now you have an idea of how an authorization server grants privileges to a token based on the scopes. In this section, you’ll see how the resource server or the microservice enforces these scopes on the resources it wants to protect. The following listing (chapter02/sample03/src/main/java/com/manning/mss/ch02/sample03/service/ResourceServerConfig.java class file) explains how the resource server enforces these rules.

Listing 2.4 ResourceServerConfig.java

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

private static final String SECURED_READ_SCOPE =
                                   "#oauth2.hasScope('read')";

private static final String SECURED_WRITE_SCOPE =
                                   "#oauth2.hasScope('write')";

private static final String SECURED_PATTERN_WRITE = "/orders/**";

private static final String SECURED_PATTERN_READ = "/orders/{id}";

@Override
public void configure(HttpSecurity http) throws Exception {

http.requestMatchers()
.antMatchers(SECURED_PATTERN_WRITE).and().authorizeRequests()
.antMatchers(HttpMethod.POST,SECURED_PATTERN_WRITE)
.access(SECURED_WRITE_SCOPE)
.antMatchers(HttpMethod.GET,SECURED_PATTERN_READ)
.access(SECURED_READ_SCOPE);
}

@Override
public void configure(ResourceServerSecurityConfigurer resources) {
       resources.resourceId("sample-oauth");
}

As you can see, the code instructs the microservice runtime (Spring Boot) to check for the relevant scope for the particular HTTP method and request path. This line of code

.antMatchers(HttpMethod.POST,
        SECURED_PATTERN_WRITE).access(SECURED_WRITE_SCOPE)

checks for the scope write for any POST request made against the request path that matches the regular expression /orders/**. Similarly, this line of code checks for the scope read for GET requests on path /orders/{id}:

.antMatchers(HttpMethod.GET,
               SECURED_PATTERN_READ).access(SECURED_READ_SCOPE)

Now try to access the POST /orders resource with the token that has only a read scope. Execute the same curl command you used last time to access this resource, but with a different token this time (one that has read access only):

> curl  -v http://localhost:8080/orders 

-H 'Content-Type: application/json' 
-H "Authorization: Bearer 47190af1-624c-48a6-988d-f4319d36b7f4" 
--data-binary @- << EOF
{
  "items":[
    {
      "itemCode":"IT0001",
      "quantity":3
    },
    {
      "itemCode":"IT0004",
      "quantity":1
    }
  ],
  "shippingAddress":"No 4, Castro Street, Mountain View, CA, USA"
}
EOF

When this command executes, you should see this error response from the resource server:

{

  "error":"insufficient_scope",
  "error_description":"Insufficient scope for this resource",
  "scope":"write"
}

This response says that the token’s scope for this particular operation is insufficient and that the required scope is write.

Assuming that you still have a valid orderId (d59dbd56-6e8b-4e06-906f-59990ce2e330) from a successful request to the POST /orders operation, try to make a GET /orders/{id} request with the preceding token to see whether it’s successful. You can use the following curl command to make this request. Note that the orderId used in the example won’t be the same orderId you got when you tried to create an order yourself. Use the one that you received instead of the one used in this example. Also make sure to replace the value of the token in the Authorization header with what you got in section 2.5.1:

> curl -H "Authorization: Bearer 47190af1-624c-48a6-988d-f4319d36b7f4" 
http://localhost:8080/orders/d59dbd56-6e8b-4e06-906f-59990ce2e330

This request should give you a successful response, as follows. The token that you obtained bears the read scope, which is what the GET /order/{id} resource requires, as declared on the resource server:

{

  "orderId":"d59dbd56-6e8b-4e06-906f-59990ce2e330",
  "items":[
    {
      "itemCode":"IT0001",
      "quantity":3
    },
    {
      "itemCode":"IT0004",
      "quantity":1
    }
  ],
  "shippingAddress":"No 4, Castro Street, Mountain View, CA, USA"
}

Throughout this chapter, we’ve covered the most primary mechanism of securing a microservice and accessing a secured microservice. As you may imagine, this chapter is only the beginning. Real-world scenarios demand a lot more than an application with a valid client ID and client secret to gain access to resources on a microservice. We discuss all these options throughout the rest of this book.

Summary

  • OAuth 2.0 is an authorization framework, which is widely used in securing microservices deployments at the edge.

  • OAuth 2.0 supports multiple grant types. The client credentials grant type, which we used in this chapter, is used mostly for system-to-system authentication.

  • Each access token issued by an authorization server is coupled with one or more scopes. Scopes are used in OAuth 2.0 to express the privileges attached to an access token.

  • OAuth 2.0 scopes are used to protect and enforce access-control checks in certain operations in microservices.

  • All samples in this chapter used HTTP (not HTTPS) endpoints to spare you from having to set up proper certificates and to make it possible for you to inspect messages being passed on the wire (network), if required. In production systems, we do not recommend using HTTP for any endpoint.


1.As you may recall from chapter 1, a JSON Web Token (JWT) is a container that carries different types of assertions or claims from one place to another in a cryptographically safe manner. If you are new to JWT, please check appendix B.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.41.214