spring-securityauthorizationkeycloakuser-permissions

Spring boot 3 : Secure RestController api's with keyclock ressource autorisations


I m working with spring boot 3 java 17 and keyclock 21.1.1. I need to secure my rest api with keyclock ressource autorisation. for that i added a key clock client

enter image description here

a client ressource enter image description here

scopes enter image description here

policy

enter image description here

and my permission

enter image description here

My RPT token

enter image description here

I'm using oauth2 dependancies

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-client</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
        </dependency>

Using @PreAuthorize("hasPermission('my_ressource', 'add')") is given me 403 forbiden

enter image description here

My Security config is

    @Configuration
    @EnableWebSecurity
    @EnableMethodSecurity
    class SecurityConfiguration {
        @Autowired
        private CorsConfigurationSource corsConfigurationSource;
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
    
            httpSecurity
                    .authorizeHttpRequests(registry -> registry.anyRequest().authenticated())
                    .oauth2ResourceServer(oauth2Configurer -> oauth2Configurer.jwt(jwtConfigurer -> jwtConfigurer.jwtAuthenticationConverter(jwt -> {
                        Map<String, Collection<String>> realmAccess = jwt.getClaim("realm_access");
                        Collection<String> roles = realmAccess.get("roles");
                        var grantedAuthorities = roles.stream()
                                .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                                .collect(Collectors.toList());
                        return new JwtAuthenticationToken(jwt,grantedAuthorities , "");
                    })))
            ;
            return httpSecurity.build();
        }
        
    }

i need to adapte my security config to use hasPermission.

Application log

    2023-09-12T17:39:18.444+01:00 DEBUG 1948 --- [nio-9000-exec-2] horizationManagerBeforeMethodInterceptor : Authorizing method invocation ReflectiveMethodInvocation: public org.springframework.http.ResponseEntity com.mypartner.tm.authentification.controller.UserBranchController.addKeyClock(); target is of class [com.mypartner.tm.authentification.controller.UserBranchController] 2023-09-12T17:39:18.452+01:00  WARN 1948 --- [nio-9000-exec-2] o.s.s.a.e.DenyAllPermissionEvaluator     : Denying user  permission 'add' on object my_ressource 2023-09-12T17:39:18.459+01:00 DEBUG 1948
    --- [nio-9000-exec-2] horizationManagerBeforeMethodInterceptor : Failed to authorize ReflectiveMethodInvocation: public org.springframework.http.ResponseEntity com.mypartner.tm.authentification.controller.UserBranchController.addKeyClock(); target is of class [com.mypartner.tm.authentification.controller.UserBranchController] with authorization manager org.springframework.security.config.annotation.method.configuration.DeferringObservationAuthorizationManager@181c058b and decision ExpressionAuthorizationDecision [granted=false, expressionAttribute=hasPermission('my_ressource', 'add')] 2023-09-12T17:39:18.470+01:00 TRACE 1948 --- [nio-9000-exec-2] o.s.s.w.a.ExceptionTranslationFilter     : Sending JwtAuthenticationToken [Principal=org.springframework.security.oauth2.jwt.Jwt@7e539329, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[ROLE_default-roles-test, ROLE_offline_access, ROLE_test_role, ROLE_uma_authorization]] to access denied handler since access is denied
    
    org.springframework.security.access.AccessDeniedException: Access Denied    at org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.attemptAuthorization(AuthorizationManagerBeforeMethodInterceptor.java:257)     at org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.invoke(AuthorizationManagerBeforeMethodInterceptor.java:198)   at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)    at net.bull.javamelody.MonitoringSpringInterceptor.invoke(MonitoringSpringInterceptor.java:76)  at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:702)  at com.mypartner.tm.authentification.controller.UserBranchController$$SpringCGLIB$$0.addKeyClock(<generated>)   at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)   at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)     at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)     at java.base/java.lang.reflect.Method.invoke(Method.java:568)   at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:207)  at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:152)  at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118)  at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:884)    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797)     at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)     at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1081)    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:974)  at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1011)  at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903)    at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:537)   at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)  at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:631)   at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:205)    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)   at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)

PS : @PreAuthorize("hasRole('test_role')") is working for me. I think that i shoud extract authorization ---> permissions : {ressource and scope } from jwt token


Solution

  • hasPermission is evaluated by a PermissionEvaluator. The default (and only) implementation provided by Spring Security is DenyAllPermissionEvaluator which will ... deny all (all expressions with hasPermission result in 403).

    You should provide with your own PermissionEvaluator bean reading from the $.authorization.permissions claim in your access tokens. Considering your @PreAuthorize("hasPermission('my_ressource', 'add')") expression, maybe something like:

    @Component
    public class KeycloakAuthorizationPermissionEvaluator implements PermissionEvaluator {
    
        @Override
        public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
            if(!(targetDomainObject instanceof String) || !(permission instanceof String)) {
                return false;
            }
            final var permissions = getAuthenticationPermissions(authentication);
            return Optional.ofNullable(permissions.get(targetDomainObject))
                    .map(KeycloakAuthorizationPermission::scopes)
                    .orElse(List.of())
                    .contains(permission);
        }
    
        @Override
        public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
            final var permissions = getAuthenticationPermissions(authentication);
            // FIXME: implement permission evaluation there too if you use this method in your expressions
            return false;
        }
        
        @SuppressWarnings("unchecked")
        static Map<String, KeycloakAuthorizationPermission> getAuthenticationPermissions(Authentication authentication) {
            if(authentication instanceof JwtAuthenticationToken jwtAuth) {
                final var authorization = (Map<String, Object>) jwtAuth.getToken().getClaims().getOrDefault("authorization", Map.of());
                final var permissions = (List<Map<String, Object>>) authorization.getOrDefault("permissions", Map.of());
                return permissions.stream().map(p -> {
                    final var rsid = Optional.ofNullable(p.get("rsid")).orElse("").toString();
                    final var rsname = Optional.ofNullable(p.get("rsname")).orElse("").toString();
                    final var scopes = (List<String>) Optional.ofNullable(p.get("scopes")).orElse(List.of());
                    return new KeycloakAuthorizationPermission(rsid, rsname, scopes);
                }).collect(Collectors.toMap(KeycloakAuthorizationPermission::rsname, p -> p));
            }
            return Map.of();
        }
        
        static final record KeycloakAuthorizationPermission(String rsid, String rsname, List<String> scopes) {}
    
    }
    

    Few notes:

    In short, you're probably over complicating things and should think again about a way to solve your problem with good old Role Based Access Control (with just Keycloak "roles", not the "authorization" features), optionally augmented with evaluations on the resource itself. Something like:

    @PutMapping("/posts/{post-id}")
    @PreAuthorize("hasAuthority('moderator') || #post.authorName == authentication.name")
    public ResponseEntity<Void> updatePost(@PathVariable("post-id") Post post, @RequestBody @Valid PostUpdateDto dto) {
        ...
    }