How to access Keycloak's AuthorizationContext from inside a PermissionEvaluator?

Viewed 801

I'm trying to achieve fine grained permissions in my application using spring boot and Keycloak.

As such i would like to be using the following annotiations:

@PreAuthorize("hasPermission('myResource', 'myScope')")

So I have implemented the following:

@Slf4j
public class KeycloakPermissionEvaluator implements PermissionEvaluator {

    @Override
    public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
        // This should probably never be the case but let's check it anyways
        if (authentication == null) {
            log.warn("Permission denied because of illegal 'authentication' argument (null)");
            return false;
        }
        // targetDomainObject will be the resourceName.
        // It can be null in case we only want to check permissions for a specific scope else it should be a String
        if (targetDomainObject != null && !(targetDomainObject instanceof String)) {
            log.warn("Permission denied because of illegal 'targetDomainObject' argument ({})", targetDomainObject.getClass().getName());
            return false;
        }
        // permission will be the scopeName.
        // It can be null in case we only want to check permissions for a specific resource else it should be a String
        if (permission != null && !(permission instanceof String)) {
            log.warn("Permission denied because of illegal 'permission' argument ({})", permission.getClass().getName());
            return false;
        }

        return hasPermission((KeycloakPrincipal) authentication.getPrincipal(), (String) targetDomainObject, (String) permission);
    }

    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        // This should probably never be the case but let's check it anyways
        if (authentication == null) {
            log.warn("Permission denied because of illegal 'authentication' argument (null)");
            return false;
        }
        // targetId needs to be set else we should be using hasPermission(Authentication, Object, Object)
        // It will be used to build the resourceName
        if (!(targetId instanceof String)) {
            log.warn("Permission denied because of illegal 'targetId' argument ({}). Did you mean to use hasPermission(resource, scope) ?", targetId == null ? "null" : targetId.getClass().getName());
            return false;
        }
        // targetType will be used to build the resourceName
        if (targetType == null) {
            log.warn("Permission denied because of illegal 'targetType' argument (null)");
            return false;
        }
        // permission will be the scopeName.
        // It can be null in case we only want to check permissions for a specific resource else it should be a String
        if (permission != null && !(permission instanceof String)) {
            log.warn("Permission denied because of illegal 'permission' argument ({})", permission.getClass().getName());
            return false;
        }

        String resourceName = String.format("%s/%s", targetType, targetId);
        return hasPermission((KeycloakPrincipal) authentication.getPrincipal(), resourceName, (String) permission);
    }

    private boolean hasPermission(KeycloakPrincipal principal, String resourceName, String scopeName) {
        AuthorizationContext authorizationContext = principal.getKeycloakSecurityContext().getAuthorizationContext();

        if (resourceName == null) {
            if (scopeName == null) {
                log.warn("Permission denied because both resourceName and scopeName are null.");
                return false;
            }
            return authorizationContext.hasScopePermission(scopeName);
        } else if (scopeName == null) {
            return authorizationContext.hasResourcePermission(resourceName);
        } else {
            return authorizationContext.hasPermission(resourceName, scopeName);
        }
    }

}

However that doesn't seem to work. authorizationContext ends up being null. Here is what the Authentication object looks like in hasPermission Authetication object

Here is my SecurityConfig

@Slf4j
@KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {

    /**
     * Registers the KeycloakAuthenticationProvider with the authentication manager.
     */
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) {
        KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
        auth.authenticationProvider(keycloakAuthenticationProvider);
    }

    /**
     * Defines the session authentication strategy.
     */
    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new NullAuthenticatedSessionStrategy();
    }

    @Override
    protected AuthenticationEntryPoint authenticationEntryPoint() {
        return (request, response, authException) -> {
            log.warn("401 Unauthorized while processing " + request.getRequestURI(), authException);
            response.sendError(401, "Unauthorized");
        };
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);

        // @formatter:off
        http
            .csrf().disable()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and().headers()
                .frameOptions().sameOrigin()
            .and().authorizeRequests()
                // Public
                .antMatchers(
                    "/v3/api-docs/**",
                    "/swagger-ui.html",
                    "/swagger-ui/**").permitAll()
                // Private
                .anyRequest().authenticated();
        // @formatter:on
    }
}

and my method security config

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
    @Autowired
    private ApplicationContext applicationContext;

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
        expressionHandler.setPermissionEvaluator(new KeycloakPermissionEvaluator());
        expressionHandler.setApplicationContext(applicationContext);

        return expressionHandler;
    }
}

And the Keycloak config:

{
  "realm": "${env.KEYCLOAK_REALM}",
  "auth-server-url": "${env.KEYCLOAK_AUTH_SERVER}",
  "ssl-required": "external",
  "enable-cors": true,
  "resource": "my-server",
  "credentials": {
    "secret": "${env.KEYCLOAK_SECRET}"
  },
  "confidential-port": 0,
  "policy-enforcer": {
    "enforcement-mode": "PERMISSIVE",
    "paths": [
      {
        "name": "Default",
        "path": "*/*"
      }
    ]
  }
}

This seems related to KEYCLOAK-12260

I have added a controller to showcase my issue:

@RestController
@RequestMapping("/test")
public class TestController {
    @Autowired
    HttpServletRequest autowWiredRequest;

    @GetMapping
    public String hello(Principal principal) {
        KeycloakAuthenticationToken principal1 = (KeycloakAuthenticationToken) this.autowWiredRequest.getUserPrincipal();
        KeycloakAuthenticationToken principal2 = (KeycloakAuthenticationToken) ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getUserPrincipal();
        KeycloakAuthenticationToken principal3 = (KeycloakAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();

        boolean allTheSame = (principal1 == principal2 && principal2 == principal3);

        KeycloakPrincipal<?> keycloakPrincipal = (KeycloakPrincipal<?>) principal1.getPrincipal();
        KeycloakSecurityContext keycloakSecurityContext = keycloakPrincipal.getKeycloakSecurityContext();
        AuthorizationContext authorizationContext = keycloakSecurityContext.getAuthorizationContext();

        return authorizationContext == null ? "null" : "OMG we're there!";
    }
}

This is what the debug looks like: enter image description here

And here is something interesting also: there seems to be another SecurityContext that contains all that I want enter image description here

1 Answers

After quite a lot of digging and some help from colleagues, finding out the fact some objects have multiple beans lead us to that page

So adding the following lines to the SecurityContext made the AuthorizationContext available.

    @Bean
    public FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean(
            KeycloakAuthenticationProcessingFilter filter) {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

    @Bean
    public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean(
            KeycloakPreAuthActionsFilter filter) {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

    @Bean
    public FilterRegistrationBean keycloakAuthenticatedActionsFilterBean(
            KeycloakAuthenticatedActionsFilter filter) {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

    @Bean
    public FilterRegistrationBean keycloakSecurityContextRequestFilterBean(
        KeycloakSecurityContextRequestFilter filter) {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

Now what I hoped to have working with this is not working yet but at least my question here is answered.

EDIT: I went a bit further and encountered some more resistance. I raised an issue directly on the Keycloak Jira: KEYCLOAK-16515

Related