Building an API Gateway with Spring Cloud, Security, Netflix Zuul, Eureka and Ribbon using JWT access and refresh tokens

ELATTAR Saad
8 min readMay 27, 2022

Let’s first define what we’re trying to achieve here before diving into establishing an API Gateway and dealing with sophisticated configuration.

An API gateway is a cloud-based management solution for APIs that sits between a client and a collection of backend services. It acts as a reverse proxy, taking all API requests, aggregating the many services required to complete them, and providing the relevant results.

This article is based on Getting started with Spring Boot: Creating a simple movies list API and Spring cloud: introduction to service discovery using Netflix eureka; Make sure to take a give it a look before getting started with this one.

The API Gateway we aiming to build will have two main jobs to handle:

  • The filtering and routing agent will manage request routing based on filtering procedures; a series of filters will determine if the request should be forwarded to the backend services, redirected, or even rejected.
  • The security agent (the largest part) is one of the filters that performs a more thorough examination of the authentication and authorisation statuses of incoming requests.

The authentication status determines whether the request sender is a registered user. The authorisation status of a request indicates whether it has access to a certain resource.

For this example I will be using:

  • Java version: 8
  • Spring boot version: 2.4.13
  • Spring cloud version: 2020.0.0

Please, note that in order to change the versions you need to check the compatibility between these elements above.

Building the security agent

The security agency will be based on the following dependencies:

  • Spring Data JPA and H2 database for storing our users:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
  • Spring Boot Web to expose endpoints to create users and login them by generating JWT Tokens (we need the JWT library also):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.18.3</version>
</dependency>
  • Spring Security which will handle the user authorisation and authentication:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
  • The Lombok dependency to reduce our boilerplate code:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

Then, let's start with a basic config of our server and datasource:

server:
port: 9090
spring:
application:
name: io-gateway

datasource:
url: jdbc:h2:file:./gateway/data/gatewayDB
driver-class-name: org.h2.Driver
username: sa
password: averystrongpassword
jpa:
defer-datasource-initialization: true
hibernate:
ddl-auto: update

The authentication statuses are based on two models: users and roles, which we’ll develop next; each module will have its own bean, controller, service, and database repository:

The user module

The user bean will be like the following:

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AppUser {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
private String username;
private String password;
@ManyToMany(fetch = FetchType.EAGER)
private Collection<Role> roles = new ArrayList<>();
}

You can other data properties if needed.

We name the bean AppUser instead of user to avoid the collision with the Spring Security user which we'll going to use later.

To handle user endpoints we created the following RESTful controller:

The user repository will be as basic as this:

The service layer of this module will be having the following specs:

As you can see, we used the UserDetailsService, which is a Spring Security default service for dealing with Spring Security users. However, because we added our own user schema, we needed additional code to handle it the Spring Security way.

Finally, we need to set and handle our custom exceptions:

The role module

The role bean will be like this:

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
}

You can add more attributes if needed.

For this module, we didn't add a controller since the user controller handle the role attribution to users, but you still can add a controller if you need dynamic roles.

To persist our roles, the roles repository handles that of us:

And the role service will handle the logic for us:

And of course we need to set our custom exceptions and a handler:

with that, we are done defining the general schema of our project (domain layer).

Spring Security configurations

Now, it's time to stuff our app with a set of needed configurations aspects, let's start with the password encoder config, the password encoder is the tool the app will use to encrypt our data (mainly passwords).

Inside of a package called "security" we created a class named SecurityBeanConfig.java:

This allows us to use the BCryptPasswordEncoder as our main password encoder, note that there're other encoders like the Pbkdf2PasswordEncoder, SCryptPasswordEncoder and the Argon2PasswordEncoder.

Next, where thing get a bit serious we need a set of configs which will allow us to define:

  • Our user service as main user service for Spring Security (I'll develop that even more later).
  • The Cross-Site Request Forgery (CSRF) and Server Session states.
  • The fully and role-based allowed and prohibited routes.
  • The authentication and authorisation filters.

Inside the same package, we created another class called SecurityConfig.java containing the following code:

Step by step we need to understand what we've done here:

  • Line 8: Because we implemented the UserDetailsService interface in our own user service, Spring security will inject our user service instead of the default service.
  • Line 9: This is where we’ll inject the encoder we created in the first stage of this config (yep, the BCryptPasswordEncoder).
  • Lines 12–14: Instead of using the default user service and encoder, we just instruct Spring Security to use ours.
  • Line 18: For demonstration reasons, we removed the CRSF protection that is enabled by default in Spring Security (since version 4) (we’ll see a suitable CRSF configuration in a future post), you may learn more about Cross-Site Request Forgery.
  • Line 19: Again, we won’t be utilizing sessions for demonstration reasons, so we set the session creation policy to stateless. You may utilize sessions (with suitable setups) by changing the policy to one of the following values: always, if a session does not already exist, one will be established ifRequired, if a session is required, the default option will be used. never, a session will never be generated, but an existing session can be utilized, or it can be stateless, spring security will never create or use a session.
  • Line 20–23: We inform Spring which routes are permitted without any authentication/authorisation (line 20), which routes require a certain role to be accessed (line 21), and which routes require the authentication state (line 23).
  • Line 24: We added the UsernamePasswordAuthenticationFilter before the authorisation filter (we'll see that next) to handle the /login requests seeking to be logged in used a username and a password.
  • Line 25: Here we add our authentication filter which we will be building next.

Building the Security filters

In this section we need build our own authentication and authorisation filter in order to control how both processes are executed.

The authentication filter

The authentication filter is the part which will handle the authentication process from the authentication attempt (sending the username and password) to the result which can be a successful or an unsuccessful authentication.

The auth function (line 8) will retrieve the username and password from the request header params, create a token and authenticate using the Spring Security AuthenticationManager.

If the authentication was successful the function (line 17) will be invoked which be charged to get the authenticated user, create the access and refresh JWT tokens and return them as a response to the /login request.

JSON Web Tokens (JWT) are a proposed Internet standard for producing data with optional signatures and/or encryption, with the payload including JSON that asserts a set of assertions. A private secret or a public/private key is used to sign the tokens. Know more about JWT.

The JwtUtils class is a utility class which we created and it has the redundant function such the tokens creation and verification functions.

With that we are done our basic security agent using Spring Security and JWT access and refresh tokens. Now, it's time to configure our Netflix Zuul reverse proxy.

Building the reverse proxy using Netflix Zuul

Starting with dependencies, for this part of our app we'll be using, Netflix Zuul as our reverse proxy, Eureka client for service discovery and Netflix Ribbon as our load balancer:

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
<version>2.2.10.RELEASE</version>
</dependency>

<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-eureka</artifactId>
<version>2.3.0</version>
</dependency>

What we need to do here is to set our routes for our backend services, for our example a GET endpoint /client/hello which return a simple string, this is a separate micro-service registered on our Eureka server under the id hello-client.

zuul:
routes:
client:
path: /client/**
serviceId: hello-client

Also, for this gateway can fetch the registered micro-service, we need to add some Eureka client config.

eureka:
instance:
hostname: ${spring.application.name}
secure-port-enabled: true
nonsecure-port-enabled: false
nonSecurePort: 80
securePort: 443
instanceId: ${eureka.instance.hostname}:${spring.application.name}:${PORT}
statusPageUrl: http://${eureka.hostname}/
healthCheckUrl: http://${eureka.hostname}/actuator/health
secureHealthCheckUrl: http://${eureka.hostname}/actuator/health
client:
registerWithEureka: false
fetchRegistry: true
serviceUrl:
# your Eureka server url
defaultZone: http://localhost:8099/eureka/

And finally to add a load balancer capability to our gateway, we'll add a simple ribbon configs:

Under the config package, we added the following config class.

With that, we are done building our app, let's get straight into testing it!

First, note that a super admin user is created when the app starts under the username "xrio" and password "verystrongpwd", when we login we must receive our two codes:

we will create another user with the name, username and password "admin":

And we assign the ADMIN role to the created user:

Case 1: when token is wrong or expired
Case 2: when token is correct

Now, we try to retrieve the list of user which need the ADMIN or SUPER_ADMIN role to be accessed:

Case 1: when token is incorrect
Case 2: when token is correct

Now, we can try accessing one of the backend micro-services from our gateway:

Case 1: when token is incorrect
Case 2: when token is correct

Finally,

The gateway we built does a good job here, but believe me when I say it needs a lot more configuration for sophisticated behavior in terms of safeguarding and managing traffic to and from the backend services.

Please notice that I skipped configurations such as CRSF and server sessions configs for demonstration reasons and to keep things simple.

--

--

ELATTAR Saad

Warm regards! I'm Saad, a software engineer. The most enjoyable part of any journey is what you share along the way, so code and document the journey!