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
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
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.
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.
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.
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 .
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.
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
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.
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.
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
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.
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
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
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.
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.
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.
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.
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.
Securing Actuator Endpoints
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.