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

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:

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