This article and the source codes attached should be used as reference only.Please thoroughly test your implementation before making changes to production environment
Checkout our NEW Video Channel you can like and subscribe too!

Introduction

The new SpringCloudGateway is a reactive version of wellknown Zull reverse proxy in Spring Cloud arena.The major advantage of this is it compatible with Spring Reactive Core so we can use FLux,Mono,WebClient of Spring5.Also if you are from PCF world you will know the SCG is standard gateway mechanism in PCF platform to proxy internal services.

Source Code

Context

In a microservice world, gateway becomes an integral part of the whole system.This is because gateway proxies the actual endpoint and act as a single cumulative service (composition pattern) exposed backed by numerous services which client doesn’t have clue at all.Not only this is a desired security model( as all of your intercommunication happens over a private network) also because of this isolation,service becomes loosely coupled.

We are here to explore a very basic but essential usecase.

  • We have a SCG,UAA,Resource1,Resource2 services.
  • SCG registers itself with UAA with authorization Code flow
  • SGG gets a token and calls the downstream service using token relay
  • The end to end flow is reactive (apart from the UAA server)

oauth2_microservice_flow.png

A small note on UAA server

UAA or User Account & Authentication is a PCF authorization and authentication service.Simply to put it is AuthServer developed using SpringBoot.We are using the UAA war and deploy it into our local servlet container tomcat. Of course, we could have build our own AuthServer,this is to just save time and focus on the purpose of this post.

Small note on sessions

This post assumes you have a basic understanding of OAtuh2 functionality.

An SCG when acts as client,after authorization and authentication against the UAA, it generates a session object in its own domain.Try not to confuse it with the session stored in UAA server domain. Both are different, like signing into your favorite website using a google account.they both maintain separate session with each other.

the reason to emphasize is, in case you logout from your authorization, you will not be logged out from your SCG domain.If you want this feature, we have implement it separately.

Project Structure

scg-projectstructure.png

Various endpoints

UAA login - http://localhost:8090/uaa/login
SCG home - http://localhost:8080/
SCG downstream - http://localhost:8080/calledForm
   calls resource server 1 
     calls resource server 2

Note that both resource 1 and 2 should not have any docker port expose to host as it will communicate through docker network not know to the outside world.So through browser we can only view UAA and SCG.In this post have intentionally made resource 2 with exposed port to host,to show you that even if it exposed you still need a token to get data from the resource.To prove the point that we have 2 layers of defense here.

version: '3'
services:
  uaa:
    image: scg-demo-uaa:latest
    container_name: uaa
    expose:
      - "8090"
    ports:
      - "8090:8090"
  secured-service:
    image: scg-demo-secured-service:latest
    container_name: resource
    expose:
      - "9000"
  secured-service2:
    image: scg-demo-secured-service2:latest
    container_name: resource2
    expose:
      - "9001"
    ports:
      - "9001:9001"
  gateway:
    image: scg-demo-security-gateway:latest
    container_name: gateway
    expose:
      - "8080"
    ports:
      - "8080:8080"
    depends_on:
      - secured-service
      - secured-service2
      - uaa
 

Deployment

We need docker desktop or any docker for our deployment.UAA is a war so no need to build.While the other 3 we have to build,create docker image and the use docker-compose up to deploy all 4.

Code walkthrough

You can get the full source code here

Lets look into the SCG configs first

  security:
    oauth2:
      client:
        registration:
          gateway:
            provider: uaa
            client-id: gateway
            client-secret: secret
            authorization-grant-type: authorization_code
            redirect-uri-template: "{baseUrl}/login/oauth2/code/{registrationId}"
            scope: openid,profile,email,resource.read
        provider:
          uaa:
            authorization-uri: http://localhost:8090/uaa/oauth/authorize //get code
            token-uri: http://uaa:8090/uaa/oauth/token //get token
            user-info-uri: http://uaa:8090/uaa/userinfo //get userinfo
            user-name-attribute: sub
            jwk-set-uri: http://uaa:8090/uaa/token_keys //validate token

Once authentication and authorization happens Spring creates AuthenticationPrincipal object.We can use a controller and annotate this Object.Spring will automatically inject this object to our controller function

 
     @GetMapping("/")
    public String index(Model model,
                        @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient,
                        @AuthenticationPrincipal OAuth2User oauth2User) {
        model.addAttribute("userName", oauth2User.getName());
        model.addAttribute("clientName", authorizedClient.getClientRegistration().getClientName());
        model.addAttribute("userAttributes", oauth2User.getAttributes());
        return "index";
    }

Now we need to call the downstream service,for this we need to use routelocator for SCG

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
                .route("resource2", r -> r.path("/calledForm")
                        .filters(f -> f.filters(filterFactory.apply())
                                .removeRequestHeader("Cookie")) // Prevents cookie being sent downstream
                        .uri("http://resource:9000"))
                .build();
    }
    

Note that we are calling the downstream endpoint directly without passing the bearer token. This is magically added by spring using token relay functionality

   @Autowired
    private TokenRelayGatewayFilterFactory filterFactory;

dependencies

We need to add webflux and gateway dependencies, also thymleaf to display the user attributes in the SCG homepage

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-webflux</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-oauth2-client</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-gateway</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.thymeleaf.extras</groupId>
			<artifactId>thymeleaf-extras-springsecurity5</artifactId>
		</dependency>
        

Resource Server 1

This service acts as a intermediatory between gateway and the resource server2.As this a resource server 2 we need mark it resource sever in config file.Next we need to use WebClient and call the downstrean resource server 2.

Security Config

  @Bean
  SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
    http
        .authorizeExchange()
          .pathMatchers("/calledForm")
            .hasAuthority("SCOPE_resource.read")
          .anyExchange()
            .authenticated()
          .and()
        .oauth2ResourceServer()
          .jwt();
    return http.build();

To spice it up more, we are going to call 2 endpoints of resource2 and call them in sequence. Remember we are using reactive streams,where calls are async by default.We make a sequence we have to make nested call like this

     @GetMapping("/calledForm")
    public Mono<ResponseEntity<String>> calledForm(@AuthenticationPrincipal Jwt jwt) {

        LOG.trace("***** JWT Claims: {}", jwt.getClaims().toString());
        LOG.trace("***** JWT Headers: {}", jwt.getHeaders());
        LOG.trace("***** JWT Claims: {}", jwt.getClaims().toString());
        LOG.trace("***** JWT Token: {}", jwt.getTokenValue());

        GreetingWebClient gwc = new GreetingWebClient(jwt.getTokenValue());
        return gwc.getResult()
                .flatMap(s ->
                        gwc.getOtherResult().flatMap(
                                t -> Mono.just(new ResponseEntity<>(t + "&" + s, HttpStatus.OK))));

    }
 

Call to downstream

         Mono<ClientResponse> result1 = client.get()
                .uri("/hello")
                //.header("Authorization", this.jwt)
                .headers(headers ->
                        headers.add("Authorization", this.jwt)
                ).accept(MediaType.TEXT_PLAIN)
                .exchange();

        return result1.flatMap(res -> res.bodyToMono(String.class));
    }
    

appplication config file not much to add, only the UAA server token validation endpoint

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: http://uaa:8090/uaa/token_keys

dependencies

We need to add resource server dependency and webflux dependency

    <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-resource-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-jose</artifactId>
        </dependency>

Resource Server 2

We have reached the bottom of our stream now.We add the two endpoints which are getting called from resource server 1.

@RestController
public class GreetingHandler {

    private static final Logger LOG = LoggerFactory.getLogger(GreetingHandler.class);

    @GetMapping("/hello")
    public Mono<ResponseEntity<String>> hello() {
        return Mono.just(new ResponseEntity<>("Hello, Spring ", HttpStatus.OK));
    }

    @GetMapping("/hello2")
    public Mono<ResponseEntity<String>> hello2() {
        return Mono.just(new ResponseEntity<>("Hello, Spring 2", HttpStatus.OK));
    }

}

rest of the configs are similar as resource server 1

Let see it in action

  1. call http://localhost:8080/calledForm
  2. redirects to http://localhost:8090/uaa/login login.PNG
  3. After successful authentication and scope grant, auth code send to SCG
  4. SCG makes server to server call gets token
  5. /calledForm calls downstream to resource1
  6. resource 1 calls resource 2 /hello and /hello2 endpoint,we see below

calledFormSCG.PNG

In console

FlowInTheConsole.PNG

Complete oauth2 object displayed in SCG using theamleaf

oauth2ObjectAfterAuth.PNG

Try to call resource server directly

resourceServer401.PNG

Conslusion

We have successfully called the downstream using the token relay pattern.The token is generated only at the gateway and relayed down.We also able to reactively call the services end-to-end.

    Content