Keycloak authorization policy evaluation with spring cloud gateway

935 Views Asked by At

I am trying to use keycloak for authorization in spring cloud gateway. Keycloak does not provide any spring based adapters for policy enforcement for reactive stack.However, it does provide an endpoint for policy evaluation.

http://localhost:8080/realms/myrealm/protocol/openid-connect/token -- POST

Request:
grant_type:urn:ietf:params:oauth:grant-type:uma-ticket
response_mode:decision
audience:b2b
permission:spm_audit#GET 

Header:
Authorization : bearer <JWT>

# spm_audit is the resource that I have created in keycloak and GET is the scope(using HTTP methods as api scopes).

RESPONSE:
{
    "result": true
}

resource created in keycloak My problem is that above endpoint does not accept URI as permission in request body and I don't have any resource-name to request URL mapping at gateway.

One possible solution could be to use gateway's route id as resource name and pass it in permission

  cloud:
    gateway:
      routes:
        - id: spm_audit
          uri: http://localhost:8001
          predicates:
          - Path=/gateway/spm/api/v1/registrations/{regUUID}/audit
          filters:
            - StripPrefix=1
          metadata:
            custom_scope: "test scope"

#Fetch the route info in auth manager
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR); //(ServerWebExchange exchange)
route.getId();

The problem with this approch is that the route matching filters are applied after authorization filter and exchange.getAttribute(GATEWAY_ROUTE_ATTR) is coming as null, plus I will have to map all api paths in route configuration and will end up with a huge configuration file.

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, @Qualifier("keycloKWebClient")WebClient kycloakWebClient) {
        http
                .authorizeExchange()
                .pathMatchers(
                        "/gateway/*/public/**")
                .permitAll()
            .and()
                .authorizeExchange()
                .anyExchange()
                .access(keyalokAuthManager(kycloakWebClient))....#this is where I call policy evaluation api

https://www.keycloak.org/docs/latest/authorization_services/index.html#_service_authorization_api

1

There are 1 best solutions below

1
On

What about using spring-security for resource-servers with a JWT decoder? It would be far more efficient as it would save many round trips to authorization-server (JWT decoder validates access-token with authorization-server public key downloaded once when policy enforcer requires a call to authorization-server for each and every incoming "non public" request).

You can map Keycloak "roles" to spring "granted authorities" and apply Role Based Access Control either with:

  • http.authorizeExchange().pathMatchers("/protected-route/foo").hasAuthority("ROLE_IN_KEYCLOAK") in your Java conf
  • @PreAuthorize("hasAnyAuthority('ROLE_IN_KEYCLOAK')") on your components methods.

For that, all you have to do is provide with an authentication converter with custom authorities converter:

    interface AuthenticationConverter extends Converter<Jwt, Mono<JwtAuthenticationToken>> {}

    interface AuthoritiesConverter extends Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> {}

    @Bean
    AuthoritiesConverter authoritiesConverter() {
        return claims -> {
            final var realmAccess = (Map<String, Object>) jwt.getClaims().getOrDefault("realm_access", Map.of());
            final var realmRoles = (Collection<String>) realmAccess.getOrDefault("roles", List.of());
            // concat client roles to following stream if your app uses client roles in addition to realm ones
            return realmRoles.stream().map(SimpleGrantedAuthority::new).toList();
        }
    }

    @Bean
    public AuthenticationConverter authenticationConverter(AuthoritiesConverter authoritiesConverter) {
        return jwt -> Mono.just(new JwtAuthenticationToken(jwt, authoritiesConverter.convert(jwt.getClaims())));
    }

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, AuthenticationConverter authenticationConverter) {
        http.oauth2ResourceServer().jwt()
            .jwtAuthenticationConverter(authenticationConverter);
    }

You should also have a look at this repo: https://github.com/ch4mpy/spring-addons

There is a spring-addons-webflux-jwt-resource-server spring-boot (2.7 or later) starter which would save you quite some configuration hassle. Tutorials in this repo are using servlet variants of the libs, but the 4 starters (servlet/reactive with JWT-decoder/introspection) work the same and you should easily find what to adapt for your reactive app.