Last updated on November 14th, 2023
In my previous post, I introduced the API Gateway Pattern in Microservices. Let’s take a look at the components in our system, as demonstrated below.
We have API Gateway which proxies our requests to the authorization server and two services in our services. The API gateway has to proxy requests from clients to the authorization server and profile and point services.
As mentioned earlier, I will attempt to implement each of the functions of the API Gateway Pattern. In this post, I will start with the implementation of security concerns using Spring Cloud Gateway. Further exploration of the remaining concerns such as observability (monitoring logging) or resiliency (rate limiter), will be covered in upcoming posts.
Start our Authorization Server
Let’s launch our Authorization Server, which was implemented in my previous post. Add a docker-compose.yml
file and start the server at the port :9000
:
docker-compose.yml
services:
auth_database:
image: postgres
container_name: auth_database
ports:
- "5432:5432"
environment:
- POSTGRES_PASSWORD=123456
- POSTGRES_DB=auth_server
restart: always
authorization:
build: ./
container_name: authorization_service
ports:
- "9000:9000"
environment:
- AUTH_POSTGRES_USER=postgres
- AUTH_POSTGRES_PASSWORD=123456
- POSTGRES_HOST=host.docker.internal
- POSTGRES_DATABASE=auth_server
depends_on:
- auth_database
Don’t forget to include both client credentials and user credentials in our database, as they will be used in the subsequent steps.
Configure the resources server
To secure the endpoints of the resources server, we first add security-related dependencies as shown below:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
I configured the jwk-set-endpoint as /oauth2/jwks with an asymmetric keys pair in the authorization server previously. Therefore, all that’s required on the resource server is to add the configuration below to discover the public keys for JWT token validation.
spring:
application:
name: profile-service
security:
oauth2:
resourceserver.jwt.jwk-set-uri: ${OAUTH2_CLIENT_ISSUER_URI:http://localhost:9000}/oauth2/jwks
The next step is a simple configuration for our SecurityFilterChain
to authorize all incoming requests and validate the token.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain configureFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(
authorizeRequests ->
authorizeRequests
.requestMatchers("/webjars/**")
.permitAll()
.anyRequest()
.authenticated())
.oauth2ResourceServer(configure -> configure.jwt(Customizer.withDefaults()));
http.csrf(AbstractHttpConfigurer::disable);
return http.build();
}
}
Finally, let’s create a simple endpoint for testing purposes, as shown below:
@RestController
@RequestMapping
public class UserController {
@GetMapping("/me")
public String getMyInfo() {
return "This is my info";
}
}
Configure Spring Cloud Gateway as an OAuth2 Client
To enable Spring Cloud Gateway to act as an OAuth2 Client, we include spring-boot-starter-oauth2-client
dependency.
<!--Security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<!-- Gateway-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
Next, we need to set up the client information in the YAML configuration as follows. registrationId
should be set to spring
, and issuer-uri
should point to our authorization server.
Remember that the client credentials should be registered to the Authorization Server above.
spring:
security:
oauth2:
client:
registration:
spring:
provider: spring
client-id: clientB
client-secret: secret
authorization-grant-type: authorization_code
client-authentication-method: client_secret_basic
redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'
scope: read,openid
provider:
spring:
issuer-uri: ${OAUTH2_CLIENT_ISSUER_URI:http://127.0.0.1:9000}
Let’s configure basic Spring Security.
The central place for defining and configuring security policies in Spring Security is a SecurityWebFilterChain
bean. It tells the framework which filters should be enabled, so we can use this bean to define and configure security policies for the application. We can build a SecurityWebFilterChain bean through DSL provided by ServerHttpSecurity.
The ServerHttpSecurity object provides a convenient DSL to configure Spring Security and build a SecurityWebFilterChain
bean. With authorizeExchange()
you can define access policies for any request (called exchange in Reactive Spring). With oauth2Login()
, you can configure an application to act as an OAuth2 Client and also authenticate users.
Next, annotate the SecurityConfig
with @EnableWebFluxSecurity
to enable authentication for all the exchanges.
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
SecurityWebFilterChain configureFilterChain(ServerHttpSecurity http) {
return http.authorizeExchange(
exchange ->
exchange
.anyExchange()
.authenticated())
.oauth2Login(Customizer.withDefaults())
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.build();
}
}
Configure routing on Spring Cloud Gateway
Introduce Spring Cloud Gateway architecture
Spring Cloud Gateway offers an alternative to traditional proxies. It is built on top of the Reactor project and provides a non-blocking API for routing and filtering HTTP requests. With SCG, we can establish routing rules and filters that have the capability to adjust or enhance the request and response before it reaches the backend services.
Let’s take a look at its overall architecture:
HandlerMapping
The Gateway Handler Mapping component is responsible for mapping incoming requests to the suitable handler based on the routing rules. When a request reaches the gateway, the first thing the gateway does is match the request with each of the routes based on the defined predicates.
Filters
The Gateway Filter component is responsible for filtering the request and response before it reaches the backend service. Filters can be used to modify the request and response headers and add authentication, logging, and monitoring.
There are many out-of-the-box filters to modify the request header and the body. Pre-filters are applied for a particular route, while global filters are applied to all the route requests. In the upcoming implementation, we will apply global filters to authentication and authorization for all requests. After the downstream service generates a response, the post-filters can be used to adjust the response. For instance, it can apply a checksum to the response header to ensure that the response is not tampered with during transit, protecting against middleman attacks.
Spring Cloud Gateway Implementation
Finally, we configure routing either through Java code or YAML, as shown below.
Through defining a @Bean
:
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.GatewayFilterSpec;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GatewayConfig {
@Bean
RouteLocator gateway(RouteLocatorBuilder rlb) {
return rlb.routes()
.route(
rs ->
rs.path("/me") // path at gateway
.filters(
gatewayFilterSpec -> {
gatewayFilterSpec.tokenRelay();
return gatewayFilterSpec.setPath("/me"); // path at profile service
})
.uri("http://localhost:8080")) // profile serverice
.build();
}
}
Or through YAML:
server:
port: 8082
spring:
cloud:
gateway:
default-filters:
- TokenRelay
routes:
- id: profile-route
uri: ${PROFILE_SERVICE_URL:http://127.0.0.1:8080}
predicates:
- Path=/me
All the setup is complete. Let’s examine how it works before testing.
After the user is authenticated, the authorization server provides the JWT to the gateway
service
. The gateway service keeps track of JWT and associates it with a specific user session using a session cookie. Subsequently, whenever the Gateway forwards a request downstream, it must include that JWT in the request. It can be done via TokenRelay
Filter. When a request comes into the gateway service, the TokenRelay
filter extracts the session from the request and retrieves the access token corresponding to the session. The resource service (profile service) only needs to validate that token for security purposes.
Testing
Let’s summarize what we have done thus far:
- 9000: Authorization server
- 8082: Gateway service
- 8080: Profile service
Here is our scenario: a user is trying to access a protected endpoint exposed by Gateway Service. The application redirects the user to a login page and asks the user to provide a username and password. Then, the Gateway Service checks the user credentials with the Authorization Service, obtains the JWT, and starts an authenticated session with the browser. The user session is kept alive through a cookie whose value is provided by the browser with each HTTP request (session cookie). Gateway Service maintains a mapping between the session identifier and access token.
First, make a call to this endpoint via the gateway:http://127.0.0.1:8082/me
. The service will redirect the user to the login page at http://127.0.0.1:9000/login
After receiving the login request, Gateway obtains the JWT from the authorization server. Subsequently, Gateway creates a web session and Authentication
bean. Finally, it returns a session ID to the external client. The external client is using a cookie with a session ID to authorize requests. Gateway
then forwards the request to the downstream Profile service.
Finally, if you take a look at the browser, you’ll notice that no token is exposed. It’s good!!! The interaction between the browser and the backend now relies on session cookies, with the backend controlling the authentication flow. This approach is recommended to mitigate the risk of token exposure.
Next steps
In this post, I’ve covered the way to set up a Spring Gateway Service as an OAuth2 Client. The source code can be found on the GitHub repo.
In the upcoming posts, I’ll sequentially implement other aspects such as monitoring or rate limiting in Spring Cloud Gateway.
Happy coding!!!