Skip to content

Lab M04P01

This lab helps you to understand how Authorization works in Spring Security. You implement integration with Open Policy Agent so teh Access decision is externalized out from the application. First, few words about OPA.

The OPA - Open Policy Agent is a service which provides access decision based on defined policy outside the service/application itself. So you can use OPA to decouple access policy from your service/application code. To have access policy managed on central place is good design strategy for more complex systems. There are more such authorization services available. Also, Keycloak IAM provides authorization services, but OPA approach is more generic and independent, and it has wider scope.

Thanks to Spring Security flexible approach, you can plug in custom Authorization into your application.

  1. Go to opa, which contains instructions, how to start OPA agent in docker container with simple library.rego script. So follow instructions and start OPA agent on localhost and port 8181.

  2. You need to implement custom Authorization code, which calls OPA agent and ask it for access decision. This application is implemented with Spring WebFlux. Open the SecurityConfig, and review the code. You need to add custom OPA AuthorizationManager implementation and plug it into Security framework.

  3. AuthorizationManager can use one or more AuthorizationDecision components which executes the decision logic itself. The OPA specific decision logic uses response from OPA agent. There is OPA client already implemented utilizing WebClient. Check the OpaClientConfiguration class for details. WebClient is used in securityFilterChain() implementation for requesting OPA agent running on localhost:8181. The toAuthorizationPayload() function uses Authentication object to retrieve principal and authorities which are used to construct request for OPA. There are input parameters (uri, principal, headers, authorities) based the OPA decides on.

  4. Add implementation of decision function into SecurityConfig which gets response from OPA and constructs AuthorizationDecision object.

         private Mono<AuthorizationDecision> toDecision(ClientResponse response) {
            if (!response.statusCode()
                    .is2xxSuccessful()) {
                return Mono.just(new AuthorizationDecision(false));
            }
    
            return response.bodyToMono(ObjectNode.class)
                    .map(node -> {
                        boolean authorized = node.path("result")
                                .path("authorized")
                                .asBoolean(false);
                        return new AuthorizationDecision(authorized);
                    });
        }
    
  5. Add implementation of ReactiveAuthorizationManager Bean. It executes OPA request and asks toDecision() function for decision.

        @Bean                                                                                                          
        public ReactiveAuthorizationManager<AuthorizationContext> opaAuthManager(WebClient opaWebClient) {                 
            return (auth, context) -> opaWebClient.post()                                                                   
                    .accept(MediaType.APPLICATION_JSON)                                                                     
                    .contentType(MediaType.APPLICATION_JSON)                                                               
                    .body(toAuthorizationPayload(auth, context), Map.class)                                                 
                    .exchangeToMono(this::toDecision);                                                                     
        }                                                                                                                  
    
  6. As the last step modify the ServerHttpSecurity configuration, and add custom OPA AuthenticationManager into exchange.

        http                                                                          
            .authorizeExchange((authz) -> authz                                       
                .pathMatchers("/library/**")                                           
                   .access(opaAuthManager(opaWebClient))                               
                   .anyExchange().permitAll()                                         
            ).httpBasic(withDefaults());                                              
    
  7. Start application and use postman to test library API. Check the OPA agent logs.

  8. Try to modify the library.rego script to made request to API authorized.

      default allow := {
         "authorized": true,
         "messages": [],
      }
    

    Note: The rego script is just a simple example. There should be more complex logic to decide based on access policy model

  9. You can try more realistic scenario with RBAC policy definition in data.json file. Check library.rego, and uncomment relevant section. Do not forget to update decission check code in the project, as response from OPA agent is changed.