© Kasun Indrasiri and Prabath Siriwardena 2018
Kasun Indrasiri and Prabath SiriwardenaMicroservices for the Enterprisehttps://doi.org/10.1007/978-1-4842-3858-5_12

12. Securing Microservices

Kasun Indrasiri1  and Prabath Siriwardena1
(1)
San Jose, CA, USA
 

In Chapter 11, “Microservices Security Fundamentals,” we discussed the common patterns and fundamentals related to securing microservices. If you haven’t gone through it, we strongly recommend you do that. In this chapter, we discuss how to implement security for microservices using Spring Boot. We explain how you can invoke a microservice directly, either as an end user or a system, secure the communication between two microservices, access controlling, and protecting access to the actuator endpoints.

Securing a Microservice with OAuth 2.0

In a typical microservices deployment, OAuth 2.0 is used for edge security (see Figure 12-1). A gateway sitting in front of a microservices deployment will validate the OAuth 2.0 access tokens and will issue its own tokens to the downstream microservices. This token can be another OAuth token issued by an internal security token service (STS), which is trusted by all the downstream microservices. If it is a self-contained access token (or JSON Web Token), then the microservice itself can validate the token by verifying its signature. If not, it has to talk to a token validation endpoint exposed by the security token service. In the examples in this chapter, we skip the gateway interactions, and the microservice, which receives the access token talks to the token issuer to validate it.
../images/461146_1_En_12_Chapter/461146_1_En_12_Fig1_HTML.jpg
Figure 12-1

Accessing a secured microservice via an API gateway

Enable Transport Layer Security (TLS)

OAuth 2.0 tokens used in Figure 12-1 are bearer tokens. Bearer tokens are like cash. If someone steals ten bucks from you, no one can prevent him or her from using the stolen money at Starbucks to buy a cup of coffee. The cashier will never challenge the person to prove the ownership of money. In the same way, anyone who steals a bearer token can use it to impersonate the owner of it and can access the resource (or the microservice). Whenever we use bearer tokens, we must use them over a secured communication channel, hence we need to enable TLS for all the communication channels shown in Figure 12-1.

Note

To run the examples in this chapter, you need Java 8 or latest, Maven 3.2 or latest, and a Git client. Once you have successfully installed those tools, you need to clone the Git repo: https://github.com/microservices-for-enterprise/samples.git . The chapter samples are in the ch12 directory.

:> git clone https://github.com/microservices-for-enterprise/samples.git

To enable TLS, first we need to create a public/private key pair. The following command uses keytool, which comes with the default Java distribution, to generate a key pair and stores it in the keystore.jks file. This file is also known as a key store and it can be in different formats. Two most popular formats are Java Key Store (JKS) and PKCS#12. JKS is specific to Java, while PKCS#12 is a standard that belongs to the family of standards defined under Public-Key Cryptography Standards (PKCS). In the following command, we specify the key store type with the storetype argument, which is set to JKS.
> keytool -genkey -alias spring -storetype JKS -keyalg RSA -keysize 2048 -keystore keystore.jks -validity 3650
The alias argument in this command specifies how to identify the generated keys stored in the key store. There can be multiple keys stored in a given key store, and the value of the corresponding alias must be unique. Here we use spring as the alias. The validity argument specifies that the generated keys are only valid for 10 years or 3650 days. The keysize and keystore arguments specify the length of the generated keys and the name of the key store where the keys are stored. The genkey option instructs the keytool to generate new keys; instead of genykey, you can also use the genkeypair option. Once this command is executed, it will prompt you to enter a key store password and will ask you to enter the data required to generate the certificate, as shown here.
Enter keystore password: XXXXXXXXX
Re-enter new password: XXXXXXXXX
What is your first and last name?
  [Unknown]:  foo
What is the name of your organizational unit?
  [Unknown]:  bar
What is the name of your organization?
  [Unknown]:  zee
What is the name of your City or Locality?
  [Unknown]:  sjc
What is the name of your State or Province?
  [Unknown]:  ca
What is the two-letter country code for this unit?
  [Unknown]:  us
Is CN=foo, OU=bar, O=zee, L=sjc, ST=ca, C=us correct?
  [no]:  yes

The certificate created in this example is known as a self-signed certificate. In other words, there is no certificate authority (CA). Typically in a production deployment, either you will use a public certificate authority or an enterprise-level certificate authority to sign the public certificate, so any client who trusts the certificate authority can verify it. If you are using certificates to secure service-to-service communication in a microservices deployment, you need not worry about having a public certificate authority. You can have your own certificate authority.

Note

For each of your microservices, you need to create a unique key store, along with a key pair. For convenience, in this chapter, we use the same key store for all our microservices.

To enable TLS for a Spring Boot microservice, copy the key store file created previously (keystore.jks) to the home directory of the sample (e.g., ch12/sample01/) and add the following to [SAMPLE_HOME]/src/main/resources/application.properties. The samples that you download from the samples Git repository already have these values. We are using springboot as the password for both the key store and the private key.
server.port: 8443
server.ssl.key-store: keystore.jks
server.ssl.key-store-password: springboot
server.ssl.keyAlias: spring
To validate that everything works fine, use the following command from the ch12/sample01/ directory to spin up the TokenService microservice. Notice the line, which prints the HTTPS port.
> mvn spring-boot:run
Tomcat started on port(s): 8443 (https) with context path "

Note

In the sections that follow, we assume the TLS is configured in all the examples, with the same key store we created here.

Setting Up an OAuth 2.0 Authorization Server

The responsibility of the authorization server is to issue tokens to its clients and respond to the validation requests from downstream microservices. This also plays the role of a security token service (STS) , as shown in Figure 12-1. There are many open source OAuth 2.0 authorization servers out there: WSO2 Identity Server, Keycloak, Gluu, and many more. In a production deployment, you may use one of them, but for this example, we are setting up a simple OAuth 2.0 authorization server with Spring Boot. It is another microservice and quite useful in developer testing. The code corresponding to the authorization server is in the ch12/sample01 directory.

Let’s start by looking at ch12/sample01/pom.xml for the notable Maven dependencies. These dependencies introduce a new set of annotations (the @EnableAuthorizationServer annotation and @EnableResourceServer annotation) to turn a Spring Boot application to an OAuth 2.0 authorization server.
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.security.oauth</groupId>
  <artifactId>spring-security-oauth2</artifactId>
</dependency>

The sample01/src/main/java/com/apress/ch12/sample01/TokenServiceApp.java class carries the @EnableAuthorizationServer annotation, which turns the project into an OAuth 2.0 authorization server. We’ve added the @EnableResourceServer annotation to the same class, as it also has to act as a resource server to validate access tokens and return the user information. It’s understandable that the terminology here is little confusing, but that’s the easiest way to implement the token validation endpoint (in fact, the user info endpoint, which also indirectly does the token validation) in Spring Boot. When you use self-contained access tokens (JWTs), this token validation endpoint is not required.

The registration of clients with the Spring Boot authorization server can be done in multiple ways. This example registers clients in the code itself, in the sample01/src/main/java/com/apress/ch12/sample01/config/AuthorizationServerConfig.java file. The AuthorizationServerConfig class extends the AuthorizationServerConfigurerAdapter class to override its default behavior. Here we set the client ID to 10101010, client secret to 11110000, available scope values to foo and/or bar, authorized grant types to client_credentials, password, and refresh_token, and the validity period of an access token to 60 seconds. Most of the terminology we use here is from OAuth 2.0 and those we explained in Chapter 11.
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
       clients.inMemory().withClient("10101010")
             .secret("11110000").scopes("foo", "bar")
             .authorizedGrantTypes("client_credentials", "password",
                                   "refresh_token")
             .accessTokenValiditySeconds(60);
}
To support password grant type, the authorization server has to connect to a user store. A user store can be a database or an LDAP server that stores user credentials and attributes. Spring Boot supports integration with multiple user stores, but once again, the most convenient one, which is just good enough for this example, is an in-memory user store. The following code from the sample01/src/main/java/com/apress/ch12/sample01/config/WebSecurityConguration.java file adds a user with the USER role to the system.
@Override
public void configure(AuthenticationManagerBuilder auth) throws
Exception {
       auth.inMemoryAuthentication()
          .withUser("peter").password("peter123").roles("USER");
}
Once we define the in-memory user store in Spring Boot, we also need to engage that with the OAuth 2.0 authorization flow, as shown next, in the code sample01/src/main/java/com/apress/ch12/sample01/config/AuthorizationServerConfig.java.
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
       endpoints.authenticationManager(authenticationManager);
}
To start the authorization server, use the following command from the ch12/sample01/ directory to spin up the TokenService microservice.
> mvn spring-boot:run
To get an access token using the client credentials OAuth 2.0 grant type, use the following command. Make sure to replace the values of $CLIENTID and $CLIENTSECRET appropriately. The hard-coded value for the client ID and client secret used in our example are 10101010 and 11110000, respectively.
> curl -v -X POST --basic -u $CLIENTID:$CLIENTSECRET -H "Content-Type: application/x-www-form-urlencoded;charset=UTF-8" -k -d "grant_type=client_credentials&scope=foo" https://localhost:8443/oauth/token
{"access_token":"81aad8c4-b021-4742-93a9-e25920587c94","token_type":"bearer","expires_in":43199,"scope":"foo"}

Note

We use the –k option in the cURL command. Since we have self-signed (untrusted) certificates to secure our HTTPS endpoint, we need to pass the –k parameter to tell cURL to ignore the trust validation. You can find more details about the parameters used here from the OAuth 2.0 6749 RFC: https://tools.ietf.org/html/rfc6749 .

To get an access token using the password OAuth 2.0 grant type, use the following command. Be sure to replace the values of $CLIENTID, $CLIENTSECRET, $USERNAME, and $PASSWORD appropriately. The hard-coded value for the client ID and client secret used in our example are 10101010 and 11110000 respectively; for username and password , we used peter and peter123.
> curl -v -X POST --basic -u $CLIENTID:$CLIENTSECRET -H "Content-Type: application/x-www-form-urlencoded;charset=UTF-8" -k -d "grant_type=password&username=$USERNAME&password=$PASSWORD&scope=foo" https://localhost:8443/oauth/token
{"access_token":"69ff86a8-eaa2-4490-adda-6ce0f10b9f8b","token_type":"bearer","refresh_token":"ab3c797b-72e2-4a9a-a1c5-c550b2775f93","expires_in":43199,"scope":"foo"}

Note

If you carefully observe the two responses we got for the OAuth 2.0 client credentials grant type and the password grant type, you might have noticed that there is no refresh token in the client credentials grant type flow. In OAuth 2.0, the refresh token is used to obtain a new access token, when the access token is expired. This is quite useful when the user is offline and the client application has no access to his/her credentials to get a new access token. In that case, the only way is to use a refresh token. For the client credentials grant type, there is no user involved, and it always has access to its own credentials, so it can be used any time it wants to get a new access token. Hence a refresh token is not required.

Now let’s see how to validate an access token by talking to the authorization server. The resource server usually does this. An interceptor running on the resource server intercepts the request, extracts the access token, and then talks to the authorization server. We see in the next section how to configure a resource server (another microservice protected with OAuth 2.0), and the following command shows how to directly talk to the authorization server to validate the access token obtained in the previous command. Be sure to replace the $TOKEN value with the corresponding access token.
> curl -k -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json"   https://localhost:8443/user
{"details":{"remoteAddress":"0:0:0:0:0:0:0:1","sessionId":null,"tokenValue":"9f3319a1-c6c4-4487-ac3b-51e9e479b4ff","tokenType":"Bearer","decodedDetails":null},"authorities":[],"authenticated":true,"userAuthentication":null,"credentials":"","oauth2Request":{"clientId":"10101010","scope":["bar"],"requestParameters":{"grant_type":"client_credentials","scope":"bar"},"resourceIds":[],"authorities":[],"approved":true,"refresh":false,"redirectUri":null,"responseTypes":[],"extensions":{},"grantType":"client_credentials","refreshTokenRequest":null},"clientOnly":true,"principal":"10101010","name":"10101010"}
This command returns the metadata associated with the access token, if the token is valid. The response is built inside the user() method of the sample01/src/main/java/com/apress/ch12/sample01/TokenServiceApp.java class, as shown in the following code snippet. With the @RequestMapping annotation, we map the /user context (from the request) to the user() method.
@RequestMapping("/user")
public Principal user(Principal user) {
       return user;
}

Note

By default, with no extensions, Spring Boot stores issued tokens in memory. If you restart the server after issuing a token and then validate it, it will result in an error response.

Protecting a Microservice with OAuth 2.0

In this section, we see how to protect a Spring Boot microservice with OAuth 2.0. In OAuth terminology, it’s a resource server. The code corresponding to the OAuth 2.0 protected Order Processing microservice is in the ch12/sample02 directory. To secure the microservice with OAuth 2.0, we add the @EnableResourceServer annotation to the sample02/src/main/java/com/apress/ch12/sample02/OrderProcessingApp.java class and point the security.oauth2.resource.user-info-uri property in the sample02/src/main/resources/application.properties file to the authorization server’s user info endpoint. The following shows the application.properties file corresponding to sample02. Note that the security.oauth2.resource.jwt.keyUri property is commented out there by default; we’ll discuss its usage later in the chapter.
server.port=9443
server.ssl.key-store: keystore.jks
server.ssl.key-store-password: springboot
server.ssl.keyAlias: spring
security.oauth2.resource.user-info-uri=https://localhost:8443/user
#security.oauth2.resource.jwt.keyUri: https://localhost:8443/oauth/token_key

Since the Order Processing microservice calls the user info endpoint over HTTPS, and we use self-signed certificates to secure the authorization server, this call will result in a trust validation error. To overcome that, we need to export the public certificate of the authorization server from ch12/sample01/keystore.jks to a new key store and set it as the trust store of the Order Processing microservice.

Use the following keytool command to export the public certificate and store it in the cert.crt file. The password used to protect keystore.jks is springboot. Instead of the export argument, which instructs the keytool to export a certificate under the given alias, you can also use the exportcert argument.
> keytool -export -keystore keystore.jks -alias spring -file cert.crt
Now use the following keytool command to create a new trust store with cert.crt. Here we use authserver as the alias to store the authorization server’s public certificate in trust-store.jks and copy the trust store to the ch12/sample02 directory (by default, you will find all the key stores and trust stores required to run the samples under the corresponding directories, when you clone the code from the sample Git repository). We use the same password, springboot, to protect trust-store.jks as well. Instead of the import argument, which instructs the keytool to import a certificate under the given alias, you can also use the importcert argument.
> keytool -import -file cert.crt -alias authserver -keystore trust-store.jks
We also need to set the location of the trust store and its password as system parameters in the code. You will find the following code snippet in the sample02/src/main/java/com/apress/ch12/sample02/OrderProcessingApp.java class, which sets the system properties.
static {
  String path = System.getProperty("user.dir");
  System.setProperty("javax.net.ssl.trustStore", path
                            + File.separator + "trust-store.jks");
  System.setProperty("javax.net.ssl.trustStorePassword", "springboot");
  HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier()
  {
      public boolean verify(String hostname, SSLSession session) {
                return true;
      }
  });
}

In addition to setting up the trust store system properties, the last few lines of code in this code snippet do something else too. Apart from the trust validation, we could also face a potential other problem while making an HTTPS connection to the authorization server. When we do an HTTPS call to a server, the client usually checks whether the common name (CN) of the server certificate matches the hostname in our server URL. For example, when we use localhost as the hostname in user-info-url (which points to the authorization server), the authorization server’s public certificate must have localhost as the common name. If not, it results in an error. This code disregards the hostname verification by overriding the verify function and returns true. Ideally in a production deployment you should use proper certificates and avoid such workarounds.

Let’s use the following command from the ch12/sample02/ directory to spin up the Order Processing microservice; it starts on HTTPS port 9443.
> mvn spring-boot:run
First, let’s try to invoke the service with the following cURL command with no valid access token, which should ideally return an error response.
> curl -k https://localhost:9443/order/11
{"error":"unauthorized","error_description":"Full authentication is required to access this resource"}
Now let’s invoke the same service with a valid access token obtained from the OAuth 2.0 authorization server. Make sure the value of $TOKEN is replaced appropriately with a valid access token.
> curl -k -H "Authorization: Bearer $TOKEN" https://localhost:9443/order/11
{"customer_id":"101021","order_id":"11","payment_method":{"card_type":"VISA","expiration":"01/22","name":"John Doe","billing_address":"201, 1st Street, San Jose, CA"},"items":[{"code":"101","qty":1},{"code":"103","qty":5}],"shipping_address":"201, 1st Street, San Jose, CA"}

If we see this response, then we’ve got our OAuth 2.0 authorization server and the OAuth 2.0 protected microservice running properly.

Securing a Microservice with Self-Contained Access Tokens (JWT)

In Chapter 11, we discussed JWT and its usage in detail. In this section, we are going to use a JWT issued from our OAuth 2.0 authorization server to access a secured microservice.

Setting Up an Authorization Server to Issue JWT

In this section, we see how to extend the authorization server we used in the previous section (ch12/sample01/) to support self-contained access tokens or JWTs. The first step is to create a new key pair along with a key store. This key is used to sign the JWTs issued from the authorization server. The following keytool command will create a new key store with a key pair.
> keytool -genkey -alias jwtkey -keyalg RSA -keysize 2048 -dname "CN=localhost" -keypass springboot -keystore jwt.jks -storepass springboot
This command creates a key store with the name jwt.jks, protected with the password springboot. We need to copy this key store to sample01/src/main/resources/. To generate self-contained access tokens, we need to set the value of the following properties in the sample01/src/main/resources/application.properties file.
spring.security.oauth.jwt: true
spring.security.oauth.jwt.keystore.password: springboot
spring.security.oauth.jwt.keystore.alias: jwtkey
spring.security.oauth.jwt.keystore.name: jwt.jks

The value of spring.security.oauth.jwt is set to false by default, and it has to be changed to true to issue JWTs. The other three properties are self-explanatory and you need to set them appropriately based on the values you used when creating the key store.

Let’s go through the notable changes in the source code to support JWTs. First, in the pom.xml file, we need to add the following dependency, which takes care of building JWTs.
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-jwt</artifactId>
</dependency>
In the sample01/src/main/java/com/apress/ch12/sample01/config/AuthorizationServerConfig.java class, we added the following method, which takes care of injecting the details about how to retrieve the private key from the key store. This private key is used to sign the JWT.
@Bean
protected JwtAccessTokenConverter jwtConeverter() {
  String pwd = environment.getProperty(
                   "spring.security.oauth.jwt.keystore.password");
  String alias = environment.getProperty(
                   "spring.security.oauth.jwt.keystore.alias");
  String keystore = environment.getProperty(
                   "spring.security.oauth.jwt.keystore.name");
  KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(
                        new ClassPathResource(keystore),
                            pwd.toCharArray());
  JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
  converter.setKeyPair(keyStoreKeyFactory.getKeyPair(alias));
  return converter;
}
In the same class file, we also set JwtTokenStore as the token store. The following function does it in a way, such that we set the JwtTokenStore as the token store only if the spring.security.oauth.jwt property is set to true in the application.properties file.
@Bean
public TokenStore tokenStore() {
  String useJwt = environment.getProperty("spring.security.oauth.jwt");
  if (useJwt != null && "true".equalsIgnoreCase(useJwt.trim())) {
       return new JwtTokenStore(jwtConeverter());
  } else {
       return new InMemoryTokenStore();
  }
}
Finally, we need to set the token store to AuthorizationServerEndpointsConfigurer, which is done in the following function, and once again, only if we want to use JWTs.
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
  String useJwt = environment.getProperty("spring.security.oauth.jwt");
  if (useJwt != null && "true".equalsIgnoreCase(useJwt.trim())) {
       endpoints.tokenStore(tokenStore()).tokenEnhancer(jwtConeverter())
                            .authenticationManager(authenticationManager);
  } else {
       endpoints.authenticationManager(authenticationManager);
  }
}
To start the authorization server, use the following command from the ch12/sample01/ directory to spin up the TokenService microservice, which now issues self-contained access tokens (JWTs).
> mvn spring-boot:run
To get an access token using the client credentials OAuth 2.0 grant type, use the following command. Be sure to replace the values of $CLIENTID and $CLIENTSECRET appropriately.
> curl -v -X POST --basic -u $CLIENTID:$CLIENTSECRET -H "Content-Type: application/x-www-form-urlencoded;charset=UTF-8" -k -d "grant_type=client_credentials&scope=foo" https://localhost:8443/oauth/token
This command will return a base64-url-encoded JWT, and the following shows the decoded version.
{ "alg": "RS256", "typ": "JWT" }
{ "scope": [ "foo" ], "exp": 1524793284, "jti": "6e55840e-886c-46b2-bef7-1a14b813dd0a", "client_id": "10101010" }

Only the decoded header and the payload are shown here; we are skipping the signature (which is the third part of the JWT). Since we used the client_credentials grant type, the JWT does not include a subject or username. It also includes the scope values associated with the token.

Protecting a Microservice with JWT

In this section, we see how to extend the Order Processing microservice we used in the previous section (ch12/sample02/) to support self-issued access tokens or JWTs. We only need to comment out the security.oauth2.resource.user-info-uri property and uncomment the security.oauth2.resource.jwt.keyUri property in the sample02/src/main/resources/application.properties file. The complete application.properties file will look like the following.
#security.oauth2.resource.user-info-uri:https://localhost:8443/user
security.oauth2.resource.jwt.keyUri: https://localhost:8443/oauth/token_key
Here the value of security.oauth2.resource.jwt.keyUri points to the public key corresponding to the private key, which is used to sign the JWT by the authorization server. It’s an endpoint hosted under the authorization server. If you just type https://localhost:8443/oauth/token_key on the browser, you will find the public key, as shown here. This is the key the resource server, or in this case, the Order Processing microservice uses to verify the signature of the JWT included in the request.
{
   "alg":"SHA256withRSA",
   "value":"-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+WcBjPsrFvGOwqVJd8vpV+gNx5onTyLjYx864mtIvUxO8D4mwAaYpjXJgsre2dcXjQ03BOLJdcjY5Nc9Kclea09nhFIEJDG3obwxm9gQw5Op1TShCP30Xqf8b7I738EHDFT6qABul7itIxSrz+AqUvj9LSUKEw/cdXrJeu6b71qHd/YiElUIA0fjVwlFctbw7REbi3Sy3nWdm9yk7M3GIKka77jxw1MwIBg2klfDJgnE72fPkPi3FmaJTJA4+9sKgfniFqdMNfkyLVbOi9E3DlaoGxEit6fKTI9GR1SWX40FhhgLdTyWdu2z9RS2BOp+3d9WFMTddab8+fd4L2mYCQIDAQAB -----END PUBLIC KEY-----"
}
Once we have a JWT access token obtained from the OAuth 2.0 authorization server, in the same as we did before, with the following cURL command, we can access the protected resource. Make sure the value of $TOKEN is replaced with a valid access token.
> curl -k -H "Authorization: Bearer $TOKEN" https://localhost:9443/order/11
{"customer_id":"101021","order_id":"11","payment_method":{"card_type":"VISA","expiration":"01/22","name":"John Doe","billing_address":"201, 1st Street, San Jose, CA"},"items":[{"code":"101","qty":1},{"code":"103","qty":5}],"shipping_address":"201, 1st Street, San Jose, CA"}

Controlling Access to a Microservice

There are multiple ways to control access to microservices. In this section, we see how to control access to different operations in a microservice based on the scopes associated with the access token and the user’s roles.

Scope-Based Access Control

Here we use self-contained access tokens (or JWTs) and first you need to have a valid JWT obtained from the OAuth 2.0 authorization server. The following command will get you a JWT access token with the scope foo. Make sure to replace the value of $CLIENTID and $CLIENTSECRET appropriately and keep the sample01 (which is our authorization server) running.
> curl -v -X POST --basic -u $CLIENTID:$CLIENTSECRET -H "Content-Type: application/x-www-form-urlencoded;charset=UTF-8" -k -d "grant_type=client_credentials&scope=foo" https://localhost:8443/oauth/token
To enable scope-based access control for the Order Processing microservice (sample02), we need to add the @EnableGlobalMethodSecurity annotation to the sample02/src/main/java/com/apress/ch12/sample02/OrderProcessingApp.java class, as shown here.
@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableResourceServer
public class OrderProcessingApp {
}
Now let’s use the @PreAuthorize annotation at the method level to enforce scope-based access control. The following method in the sample02/src/main/java/com/apress/ch12/sample02/service/OrderProcessing.java class requires the bar scope to access it.
@PreAuthorize("#oauth2.hasScope('bar')")
@RequestMapping(value = "/{id}", method= RequestMethod.GET)
public ResponseEntity<?> getOrder(@PathVariable("id") String orderId) {
}
Let’s try run the following cURL command, with the previously obtained JWT access token, which only has the foo scope. The command should result in an error, since our token does not carry the required scope value.
> curl -k -H "Authorization: Bearer $TOKEN" https://localhost:9443/order/11
{"error":"access_denied","error_description":"Access is denied"}

Role-Based Access Control

Just like in the previous section, here we have to use self-contained access tokens (or JWTs) and first you need to have a valid JWT obtained from the OAuth 2.0 authorization server.

The following cURL command will give you a JWT access token using the password grant type. Be sure to replace the values of $CLIENTID, $CLIENTSECRET, $USERNAME, and $PASSWORD appropriately. Unlike in the scope-based scenario, the client credentials grant type won’t work here, as by default, no roles are associated with clients (only with users).
> curl -v -X POST --basic -u $CLIENTID:$CLIENTSECRET -H "Content-Type: application/x-www-form-urlencoded;charset=UTF-8" -k -d "grant_type=password&username=$USERNAME&password=$PASSWORD&scope=foo" https://localhost:8443/oauth/token
To enable role-based access control for the Order Processing microservice (sample02), we need to add the @EnableGlobalMethodSecurity annotation to the sample02/src/main/java/com/apress/ch12/sample02/OrderProcessingApp.java class, as shown here (this is the same step we did while setting up scope-based access control in the previous section).
@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableResourceServer
public class OrderProcessingApp {
}
Now let’s use the @PreAuthorize annotation at the method level to enforce role-based access control. The following method in the sample02/src/main/java/com/apress/ch12/sample02/service/OrderProcessing.java class requires the USER role to access it. By default, we add the USER role to the user named peter, from our authorization server. Hence an access token issued to him should be good enough to access this operation.
@PreAuthorize("hasRole(USER)")
@RequestMapping(value = "/{id}", method= RequestMethod.GET)
public ResponseEntity<?> getOrder(@PathVariable("id") String orderId) {
}
Let’s try to run the following cURL command , with the previously obtained JWT access token, which only has the foo scope. If it’s issued to a user having the USER role it should result in a successful response.
> curl -k -H "Authorization: Bearer $TOKEN" https://localhost:9443/order/11
Finally, if we want to control access to a method both by the scope and the role, we can use the following annotation against the corresponding method.
@PreAuthorize("#oauth2.hasScope('bar') and hasRole('USER')")

Securing Service-to-Service Communication

In the previous section, we discussed how to set up an OAuth 2.0 authorization and secure a microservice with OAuth 2.0. In this section, we see how to invoke one microservice from another securely. We follow two approaches here—one is based on JWT and the other is based on TLS mutual authentication.

Service-to-Service Communication Secured with JWT

In this section, we see how to invoke a microservice secured with OAuth 2.0 from another microservice by passing a JWT.

Figure 12-2 extends Figure 12-1 by introducing the Inventory microservices. Upon receiving an order, the Order Processing microservice talks to the Inventory microservice to update the inventory. Here, the Order Processing microservice passes the access token it gets to the Inventory microservice.
../images/461146_1_En_12_Chapter/461146_1_En_12_Fig2_HTML.jpg
Figure 12-2

Protecting service-to-service communication with OAuth 2.0

The token validation step in Figure 12-2 will change if we use self-contained (JWT) access tokens. In that case, there is no validation call from the microservice to the authorization server (or the security token issuer). Each microservice will fetch the corresponding public key from the authorization server and will validate the signature of the JWT locally.

In this example, we use JWT access tokens. In previous sections we have already configured the authorization server (sample01) and the Order Processing microservice (sample02) to work with JWT. Now let’s see how to set up the Inventory microservice. The code corresponding to the Inventory microservice is in the ch12/sample03 directory. The way it works is almost same as the Order Processing microservice in sample02. To enable JWT authentication, make sure the following property is uncommented in the sample03/src/main/resources/application.properties file.
security.oauth2.resource.jwt.keyUri: https://localhost:8443/oauth/token_key
Now we can start the Inventory microservice with the following Maven command, run from the sample03 directory. The service starts on HTTPS port 10443.
> mvn spring-boot:run
To execute the end-to-end flow, we need to place an order at the Order Processing (sample02) microservice. It will talk only to the Inventory (sample03) microservice. Before we run our cURL client against the Order Processing microservice, let’s look at its code, which talks to the Inventory microservice. We use OAuth2RestTemplate to talk to the Inventory microservice, which will automatically pass the access token (Order Processing microservice) it got from the client application. The corresponding code is available in the sample02/src/main/java/com/apress/ch12/sample02/client/InventoryClient.java file. The Order Processing microservice gets the complete order from the client application, then extracts the list of items from the request, and updates the inventory by talking to the Inventory microservice.
@Component
public class InventoryClient {
  @Autowired
  OAuth2RestTemplate restTemplate;
  public void updateInventory(Item[] items) {
       URI uri = URI.create("https://localhost:10443/inventory");
       restTemplate.put(uri, items);
  }
}
Assuming the OAuth 2.0 authorization server (sample01) is running, let’s run the following cURL command to get an access token. Be sure to replace the values of $CLIENTID, $CLIENTSECRET, $USERNAME, and $PASSWORD appropriately. Also note that we pass both foo and bar as scope values.
> curl -v -X POST --basic -u $CLIENTID:$CLIENTSECRET -H "Content-Type: application/x-www-form-urlencoded;charset=UTF-8" -k -d "grant_type=password&username=$USERNAME&password=$PASSWORD&scope=foo bar" https://localhost:8443/oauth/token
This will produce a JWT access token. Along with that token, let’s place an order by talking to the Order Processing microservice (sample02) by running the following cURL command.
curl -v  -k -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{"customer_id":"101021","payment_method":{"card_type":"VISA","expiration":"01/22","name":"John Doe","billing_address":"201, 1st Street, San Jose, CA"},"items":[{"code":"101","qty":1},{"code":"103","qty":5}],"shipping_address":"201, 1st Street, San Jose, CA"}' https://localhost:9443/order

If all works fine, we should see the 201 HTTP status code at the cURL client and the order numbers printed on the terminal that runs the Inventory microservice.

Service-to-Service Communication Secured with TLS Mutual Authentication

In this section, we see how to enable TLS mutual authentication between the Order Processing microservice and the Inventory microservice. In most cases, TLS mutual authentication is used to enable server-to-server authentication, while JWT is used to pass the user context between microservices.

One primary requirement to enable mutual authentication is that each service should have its own key store (keystore.jks) and a trust store (trust-store.jks). Even though both are key stores, we use the term key store specifically to highlight the key store that stores the server’s private/public key pair, while the trust store carries the public certificates of the trusted servers and clients. For example, when the Order Processing microservice talks to the Inventory microservice, which is secured with TLS mutual authentication, the public certificate of the certificate authority that signs the Order Processing microservice’s public key must be in the trust store (sample03/trust-store.jks) of the Inventory microservice.

Since we use self-signed certificates here, we have no certificate authority, hence we include the public certificate itself. The Order Processing microservice, which acts as the TLS mutual authentication client, uses its keys from the key store (sample02/keystore.jks) during the TLS handshake for authentication. It also has to store the public certificate of the certificate authority that signs the Inventory microservice’s public key, in its trust store (sample02/trust-store.jks). Once again, since we do not have a certificate authority, we store the public certificate itself. Figure 12-3 illustrates what we discussed here.
../images/461146_1_En_12_Chapter/461146_1_En_12_Fig3_HTML.jpg
Figure 12-3

Distribution of certificates to facilitate TLS mutual authentication

Let’s revisit the steps involved in setting up the key stores in the Order Processing microservice. First, make sure that we have the key store at sample02/keystore.jks and the trust store at sample02/trust-store.jks. The following properties related to both the key stores are set appropriately in the sample02/src/main/java/com/apress/ch12/sample02/OrderProcessingApp.java file. Also, we already added the public certificate of the Inventory microservice to the sample02/trust-store.jks file.
System.setProperty("javax.net.ssl.trustStore", path + File.separator + "trust-store.jks");
System.setProperty("javax.net.ssl.trustStorePassword", "springboot")
System.setProperty("javax.net.ssl.keyStore",  path + File.separator + "keystore.jks");
System.setProperty("javax.net.ssl.keyStorePassword", "springboot");

Once we have the javax.net.ssl.keyStore and javax.net.ssl.keyStorePassword system properties set, the client automatically picks the corresponding key pair to respond to the server’s challenge to request the client certificate, during the TLS handshake.

Now, let’s look at the server-side configurations. The Inventory microservice should have its key store under /sample03/keystore.jks and trust store under sample03/trust-store.jks. The following properties related to trust store are set appropriately in the sample03/src/main/java/com/apress/ch12/sample03/InventoryApp.java file. Here we have only set the trust store properties. There is no need to set properties related to the key store unless it’s acting as a TLS client .
System.setProperty("javax.net.ssl.trustStore", path + File.separator + "trust-store.jks");
System.setProperty("javax.net.ssl.trustStorePassword", "springboot");
To enable TLS mutual authentication for the Inventory microservice, set the following property in the sample03/src/main/resources/application.properties file.
server.ssl.client-auth:need
Now we can test the end-to-end flow by invoking the Order Processing microservice with a valid access token. To cater to the request, the Order Processing microservice talks to the Inventory microservice, which is protected with TLS mutual authentication .
curl -v  -k -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{"customer_id":"101021","payment_method":{"card_type":"VISA","expiration":"01/22","name":"John Doe","billing_address":"201, 1st Street, San Jose, CA"},"items":[{"code":"101","qty":1},{"code":"103","qty":5}],"shipping_address":"201, 1st Street, San Jose, CA"}' https://localhost:9443/order

Securing Actuator Endpoints

Spring Boot provides out-of-the-box monitoring capabilities via the actuators. For any Spring Boot microservice, you can add the following dependency to activate the actuator endpoints.
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
A sample microservice project with this dependency is available at ch12/sample04. Let’s spin up the Order Processing microservice from the sample04 directory with the following command. The service will start on HTTPS port 8443.
> mvn spring-boot:run
Since we have not engaged in any security at this point, if we run the following cURL command, it will successfully return a response.
> curl -k https://localhost:8443/health
{"status":"UP"}>
To enable security, we need to add the following dependency to the sample04/pom.xml file.
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
Now, uncomment the @Configuration class-level annotation in the sample04/src/main/java/com/apress/ch12/sample04/config/SecurityConfig.java file. By default it was kept commented, so that Spring Boot won’t pick the overridden configuration in that class and we can try the non-secured scenario. In the same class, the following code snippet introduces a user called admin to the system with the ACTUATOR role. When the actuator endpoints are secured, only the users in the ACTUATOR role will be able to invoke the endpoints.
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
  auth.inMemoryAuthentication().withUser("admin")
                               .password("admin").roles("ACTUATOR");
}
Now restart the service and try the same cURL command with admin/admin credentials.
> curl –k --basic -u admin:admin  https://localhost:8443/health
{"status":"UP"}

Summary

In Chapter 11, we discussed the common patterns and fundamentals related to securing microservices. This chapter focused on building those patterns with microservices developed in Spring Boot. In the next chapter, we discuss the role of observability in a microservices deployment.

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

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