Spring Cloud Gateway to enhance JWT tokens from external OAuth Authorization Server

Viewed 1158

I have the following Securing Services with Spring Cloud Gateway working with Keycloak. Expanding upon this I am routing to many Webflux Resources Servers.

However, the scopes provided by external Authorization servers are very simplistic. So I need to take the micro-service ID plus the User ID from the JWT and then check a database which then translates to their Team, which would then provide a list of accessible static pages and URL REST endpoints. So my resulting code would like something like;

...
  .matcher("/page1.html").hasAuthority("SERVICE1_PAGE1")
  .matcher("/page2.html").hasAuthority("SERVICE1_PAGE2")


@PreXXX("hasAuthority('SERVICE1_getAll'))
public List<String> getAll() {...}

I assume that I could do it at the micro-service level by Extracting Authorities Manually, but by doing this I would need to duplicate this code in every micro-service.

Edit: This is what I am currently doing, by using a WebClient to call another common micro-service which returns the corrected authorities. However, if the URL of the authority micro-service is a Gateway address, the Gateway never attempts to resolve the URL. If I use it's explicit URL, I get 401 from the authority micro-service, even though I use ServerBearerExchangeFilterFunction. If I put permitAll() on the micro-service which returns the authorities, it works.

I could stand up my own Authorization server and use a TokenEnhancer. However, by doing so, I assume that I would need to load up all possible permissions for the user for every possible micro-service (as I do not know at this point where the user is going) and could result in a lot of data.

Ideally, I would like to centralise this in the Gateway and have the TokenRelay filter recognise the route and somehow enhance the token. Is this possible?

Can someone please recommend how best to go about this?

1 Answers

This is what I finally implemented using Spring 2.2.2 Cloud Gateway, Eureka and KeyCloak.

SECURITY micro-service.

@GetMapping(value = "/grantedAuthorities/{applicationName}/{userId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    // JWT not sent when called within jwtAuthenticationConverter, so pass user id as param !!
public Flux<String> getUsersApplicationAuthorities(@PathVariable String applicationName, @PathVariable String userId) {
    return Flux.fromIterable(roleRepository.getRolesByUserId(applicationName, userId));
}

This would return something like;

SCREEN1_READ
SCREEN2_WRITE

My OTHER micro-service.

public class SecurityConfig {
    @Bean
    @Order(SecurityProperties.BASIC_AUTH_ORDER)
    SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http
            .authorizeExchange(exchanges ->
                exchanges                   
                    .pathMatchers("/screen1.html").hasAnyAuthority("SCREEN1_READ", "SCREEN1_WRITE")
                    .pathMatchers("/screen2.html").hasAnyAuthority("SCREEN1_READ", "SCREEN2_WRITE")
                    .anyExchange().authenticated()
            )
            .oauth2ResourceServer(spec ->
                spec.jwt().jwtAuthenticationConverter(jwt -> {

                /* ServerBearerExchangeFilterFunction does not work here! So I have to send userId instead of JWT */

                    WebClient webClient = loadBalancedWebClientBuilder().build();
                    String userId =  jwt.getClaimAsString("preferred_username").toUpperCase();
                    String uri = "lb://SECURITY/drs/grantedAuthorities/" + applicationName + "/" + userId;
                    return webClient.get().uri(uri)
                            .retrieve()
                            .bodyToFlux(String.class)
                            .map(s -> new SimpleGrantedAuthority(s))
                            .doOnNext(l -> log.info("Has authority " + l))
                            .collectList()
                            .map(gaList -> new JwtAuthenticationToken(jwt, gaList));
                })
            )
            .csrf().disable()
            .cors().disable();

        return http.build();
    }
}

For enabling/disabling buttons when in READ/WRITE for Thymeleaf;

@Controller
public class PageController {
    @GetMapping("/screen1.html")
    public String index(@AuthenticationPrincipal JwtAuthenticationToken jwt, Model model) {
        model.addAttribute("update", jwt.getAuthorities().contains(new SimpleGrantedAuthority("SCREEN1_WRITE"))); 
        return "screen1";
    }
}
<html>
    <body>
        <div id="updateableScreen" th:attr="data-update=${update}"></div>   
    </body>
</html>

After which use JQuery/Javascript to change access to form controls by checking $("#updateableScreen")

Related