1. Introduction
Simply put, a microservice architecture allows us to break up our system and our API into a set of self-contained services, which can be deployed fully independently.
While this is great from a continuous deployment and management point of view, it can quickly become convoluted when it comes to API usability. With different endpoints to manage, dependent applications will need to manage CORS (Cross-Origin Resource Sharing) and a diverse set of endpoints.
Zuul is an edge service that allows us to route incoming HTTP requests into multiple backend microservices. For one thing, this is important for providing a unified API for consumers of our backend resources.
Basically, Zuul allows us to unify all of our services by sitting in front of them and acting as a proxy. It receives all requests and routes them to the correct service. To an external application, our API appears as a unified API surface area.
In this tutorial, we’ll talk about how we can use it for this exact purpose, in conjunction with an OAuth 2.0 and JWTs, to be the front line for securing our web services. Specifically, we’ll be using the Password Grant flow to obtain an Access Token to the protected resources.
A quick but important note is that we’re only using the Password Grant flow to explore a simple scenario; most clients will more likely be using the Authorization Grant flow in production scenarios.
2. Adding Zuul Maven Dependencies
Let’s begin by adding Zuul to our project. We do this by adding the spring-cloud-starter-netflix-zuul artifact:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
3. Enabling Zuul
The application that we’d like to route through Zuul contains an OAuth 2.0 Authorization Server which grants access tokens and a Resource Server which accepts them. These services live on two separate endpoints.
We’d like to have a single endpoint for all external clients of these services, with different paths branching off to different physical endpoints. To do so, we’ll introduce Zuul as an edge service.
To do this, we’ll create a new Spring Boot application, called GatewayApplication. We’ll then simply decorate this application class with the @EnableZuulProxy annotation, which will cause a Zuul instance to be spawned:
@EnableZuulProxy
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
4. Configuring Zuul Routes
Before we can go any further, we need to configure a few Zuul properties. The first thing we’ll configure is the port on which Zuul is listening for incoming connections. That needs to go into the /src/main/resources/application.yml file:
server:
port: 8080
Now for the fun stuff, configuring the actual routes that Zuul will forward to. To do that, we need to note the following services, their paths and the ports that they listen on.
The Authorization Server is deployed on: http://localhost:8081/spring-security-oauth-server/oauth
The Resource Server is deployed on: http://localhost:8082/spring-security-oauth-resource
The Authorization Server is an OAuth identity provider. It exists to provide authorization tokens to the Resource Server, which in turn provides some protected endpoints.
The Authorization Server provides an Access Token to the Client, which then uses the token to execute requests against the Resource Server, on behalf of the Resource Owner. A quick run through the OAuth terminology will help us keep these concepts in view.
Now let’s map some routes to each of these services:
zuul:
routes:
spring-security-oauth-resource:
path: /spring-security-oauth-resource/**
url: http://localhost:8082/spring-security-oauth-resource
oauth:
path: /oauth/**
url: http://localhost:8081/spring-security-oauth-server/oauth
At this point, any request reaching Zuul on localhost:8080/oauth/** will be routed to the authorization service running on port 8081. Any request to localhost:8080/spring-security-oauth-resource/** will be routed to the resource server running on 8082.
5. Securing Zuul External Traffic Paths
Even though our Zuul edge service is now routing requests correctly, it’s doing so without any authorization checks. The Authorization Server sitting behind /oauth/*, creates a JWT for each successful authentication. Naturally, it’s accessible anonymously.
The Resource Server – located at /spring-security-oauth-resource/**, on the other hand, should always be accessed with a JWT to ensure that an authorized Client is accessing the protected resources.
First, we’ll configure Zuul to pass through the JWT to services that sit behind it. In our case here, those services themselves need to validate the token.
We do that by adding sensitiveHeaders: Cookie,Set-Cookie.
This completes our Zuul configuration:
server:
port: 8080
zuul:
sensitiveHeaders: Cookie,Set-Cookie
routes:
spring-security-oauth-resource:
path: /spring-security-oauth-resource/**
url: http://localhost:8082/spring-security-oauth-resource
oauth:
path: /oauth/**
url: http://localhost:8081/spring-security-oauth-server/oauth
After we’ve got that out of the way, we need to deal with authorization at the edge. Right now, Zuul will not validate the JWT before passing it on to our downstream services. These services will validate the JWT themselves, but ideally, we’d like to have the edge service do that first and reject any unauthorized requests before they propagate deeper into our architecture.
Let’s set up Spring Security to ensure that authorization is checked in Zuul.
First, we’ll need to bring in the Spring Security dependencies into our project. We want spring-security-oauth2 and spring-security-jwt:
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.9.RELEASE</version>
</dependency>
Now let’s write a configuration for the routes we want to protect by extending ResourceServerConfigurerAdapter:
@Configuration
@Configuration
@EnableResourceServer
public class GatewayConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(final HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/oauth/**")
.permitAll()
.antMatchers("/**")
.authenticated();
}
}
The GatewayConfiguration class defines how Spring Security should handle incoming HTTP requests through Zuul. Inside the configure method, we’ve first matched the most restrictive path using antMatchers and then allowed anonymous access through permitAll.
That is all requests coming into /oauth/** should be allowed through without checking for any authorization tokens. This makes sense because that’s the path from which authorization tokens are generated.
Next, we’ve matched all other paths with /**, and through a call to authenticated insisted that all other calls should contain Access Tokens.
6. Configuring the Key Used for JWT Validation
Now that the configuration is in place, all requests routed to the /oauth/** path will be allowed through anonymously, while all other requests will require authentication.
There is one thing we’re missing here though, and that’s the actual secret required to verify that the JWT is valid. To do that, we need to provide the key (which is symmetric in this case) used to sign the JWT. Rather than writing the configuration code manually, we can use spring-security-oauth2-autoconfigure.
Let’s start by adding the artifact to our project:
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
Next, we need to add a few lines of configuration to our application.yaml file to define the key used to sign the JWT:
security:
oauth2:
resource:
jwt:
key-value: 123
The line key-value: 123 sets the symmetric key used by the Authorization Server to sign the JWT. This key will be used by spring-security-oauth2-autoconfigure to configure token parsing.
It’s important to note that, in a production system, we shouldn’t use a symmetric key, specified in the source code of the application. That naturally needs to be configured externally.
7. Testing the Edge Service
7.1. Obtaining an Access Token
Now let’s test how our Zuul edge service behaves – with a few curl commands.
First, we’ll see how we can obtain a new JWT from the Authorization Server, using the password grant.
Here we exchange a username and password in for an Access Token. In this case, we use ‘john‘ as the username and ‘123‘ as the password:
curl -X POST \
http://localhost:8080/oauth/token \
-H 'Authorization: Basic Zm9vQ2xpZW50SWRQYXNzd29yZDpzZWNyZXQ=' \
-H 'Cache-Control: no-cache' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=password&password=123&username=john'
This call yields a JWT token which we can then use for authenticated requests against our Resource Server.
Notice the “Authorization: Basic…” header field. This exists to tell the Authorization Server which client is connecting to it.
It’s to the Client (in this case the cURL request) what the username and password are to the user:
{
"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpX...",
"token_type":"bearer",
"refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpX...",
"expires_in":3599,
"scope":"foo read write",
"organization":"johnwKfc",
"jti":"8e2c56d3-3e2e-4140-b120-832783b7374b"
}
7.2. Testing a Resource Server Request
We can then use the JWT we retrieved from the Authorization Server to now execute a query against the Resource Server:
curl -X GET \
curl -X GET \
http:/localhost:8080/spring-security-oauth-resource/users/extra \
-H 'Accept: application/json, text/plain, */*' \
-H 'Accept-Encoding: gzip, deflate' \
-H 'Accept-Language: en-US,en;q=0.9' \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV...' \
-H 'Cache-Control: no-cache' \
The Zuul edge service will now validate the JWT before routing to the Resource Server.
This then extracts key fields from the JWT and checks for more granular authorization before responding to the request:
{
"user_name":"john",
"scope":["foo","read","write"],
"organization":"johnwKfc",
"exp":1544584758,
"authorities":["ROLE_USER"],
"jti":"8e2c56d3-3e2e-4140-b120-832783b7374b",
"client_id":"fooClientIdPassword"
}
8. Security Across Layers
It’s important to note that the JWT is being validated by the Zuul edge service before being passed into the Resource Server. If the JWT is invalid, then the request will be denied at the edge service boundary.
If the JWT is indeed valid on the other hand, the request is passed on downstream. The Resource Server then validates the JWT again and extracts key fields such as user scope, organization (in this case a custom field) and authorities. It uses these fields to decide what the user can and can’t do.
To be clear, in a lot of architectures, we won’t actually need to validate the JWT twice – that’s a decision you’ll have to make based on your traffic patterns.
For example, in some production projects, individual Resource Servers may be accessed directly, as well as through the proxy – and we may want to verify the token in both places. In other projects, traffic may be coming only through the proxy, in which case verifying the token there is enough.
9. Summary
As we’ve seen Zuul provides an easy, configurable way to abstract and define routes for services. Together with Spring Security, it allows us to authorize requests at service boundaries.
Finally, as always, the code is available over on Github.