Keycloak policy enforcement with spring cloud gateway

65 Views Asked by At

I am working with Spring Cloud Gateway, built on Spring Webflux, and I aim to implement Keycloak's policy enforcement for authorization.

I'm using the following Keycloak adapter:

<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-spring-security-adapter</artifactId>
    <version>22.0.5</version>
</dependency>

This adapter transitively includes the following dependencies:

<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-policy-enforcer</artifactId>
</dependency>
<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-core</artifactId>
</dependency>

However, the Keycloak adapter only provides ServletPolicyEnforcerFilter, which is designed for servlet-based Spring applications.

For non-reactive applications, the policy enforcement approach is detailed in this repository: Keycloak-Quickstarts

In this approach, the Policy Enforcer requests an RPT (Requesting Party Token) from Keycloak and uses it to authorize the user for the requested resource and scope. Reusing the RPT minimizes the need for repeated interactions with Keycloak for each request.

However, as I am using Spring Cloud Gateway, I've implemented the Policy Enforcement differently:

  • Created resources, role-based policies, and permissions in Keycloak.
  • Maintained a map of request URI paths and resource names defined in Keycloak.
  • Implemented a CustomReactiveAuthorizationManager by implementing the ReactiveAuthorizationManager interface.Here Iam doing the following things
  1. Extracted the resource name from the static resource map using the request URI.
  2. Made a REST call to Keycloak to evaluate whether the current user is authorized for the requested resource and scope. Here's a snippet of the CustomReactiveAuthorizationManager:
public class CustomReactiveAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {

    private final String BEARER = "Bearer ";
    private final AuthorizationConfigProperties authorizationConfigProperties;

    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext context) {
        String path = context.getExchange().getRequest().getPath().toString();
        String token = context.getExchange().getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
        if (!ObjectUtils.isEmpty(token) && token.startsWith(BEARER)) {
            token = token.replace(BEARER, "");
            String scope = context.getExchange().getRequest().getMethod().name();
            return evaluateResourceAccess(path, token, scope);
        }
        return Mono.just(new AuthorizationDecision(false));
    }
    
     public boolean evaluateResourceAccess(String uriPath, String token, String scope) {
        String resource = authorizationConfigProperties.getResource(uriPath); //from the resource map i created in application.yml I'm getting the resource name using requestUriPath 
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        headers.setBearerAuth(token);
        MultiValueMap<String, String> requestParams = new LinkedMultiValueMap<>();
        requestParams.add("grant_type", "urn:ietf:params:oauth:grant-type:uma-ticket");
        requestParams.add("audience", clientId);
        requestParams.add("permission", resource + "#" + scope);
        requestParams.add("response_mode", "decision");
        HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(requestParams, headers);
        try {
            //making a rest call to keycloak evalute endpoint
            ResponseEntity<Object> responseEntity = restTemplate.postForEntity(keycloakTokenEndpoint, requestEntity, Object.class);
            return responseEntity.getStatusCode().is2xxSuccessful();
        } catch (HttpClientErrorException.Forbidden e) {
            log.error("UnAuthorized:" + e.getMessage());
            return false;
        } catch (Exception ex) {
            log.error("Authorization Failed:" + ex.getMessage());
            return false;
        }
    }

}

  • Integrated this custom manager into the SecurityWebFilterChain.
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    return http.csrf(ServerHttpSecurity.CsrfSpec::disable)
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
            .authorizeExchange(exchange -> exchange
                            .pathMatchers("/public-endpoint")
                            .permitAll()
                            .anyExchange()
                            .access(new CustomReactiveAuthorizationManager(authService)))
            .build();
}

Now, my concern is the number of API calls to Keycloak for evaluating access and maintaining the map of request_uri_path and resource. For each request, a call to Keycloak is made, potentially causing performance issues.

While I considered the RPT approach, it also requires maintaining the path and resource map, and explicitly checking whether the requested resource and scope are present in the RPT.Also I need the centralized authorization in spring cloud gateway I don't want to delegate the authorization to each individual microservice

I am seeking advice on alternative approaches to achieve policy enforcement in Spring Cloud Gateway while minimizing the number of Keycloak API calls.

Is there any other approach that can help optimize this scenario?

0

There are 0 best solutions below