2

I am trying to implement authorization for this use case:

  • user can access only his own resources
  • admin can access everything

I am trying out Keycloak and it's resource server. For testing purposes and to understand these scopes and permissions and stuff, I have created test client weather-api and one resource Weather with url /weatherforecast.

Then I have scope weather:read and policy that every user with role weatherer can read that resource.

Now when I try to evaluate on a user with that role, I get PERMIT: Evaluate - permit

and another user without this role gets DENY.

so I guess my policies and permissions are set correctly.

When I try to use this from my service with user-managed-access disabled, I get permit too.

But when I enable user-managed-access, it fails.

I see in debug log that it gets permissions token:

{
  ...
  "permissions": [
    {
      "scopes": [
        "weather:read"
      ],
      "rsid": "ae5ac493-b7dc-481e-9204-a664d1558a51"
    }
  ],
  ...
}

but then the next message is

Policy enforcement result for path [http://192.168.0.9:5001/weatherforecast] is : DENIED

I tried to debug Keycloak library and found something I don't really understand.

In KeycloakAdapterPolicyEnforcer this part of code:

@Override
protected boolean isAuthorized(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, AccessToken accessToken, OIDCHttpFacade httpFacade, Map<String, List<String>> claims) {
  AccessToken original = accessToken;

  if (super.isAuthorized(pathConfig, methodConfig, accessToken, httpFacade, claims)) {
      return true;
  }

  accessToken = requestAuthorizationToken(pathConfig, methodConfig, httpFacade, claims);

  if (accessToken == null) {
      return false;
  }
  ...
}

private AccessToken requestAuthorizationToken(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade httpFacade, Map<String, List<String>> claims) {
  if (getEnforcerConfig().getUserManagedAccess() != null) {
      return null;
  }
  ...
}

so when UserManagedAccess is not null, requestAuthorizationToken returns null and then it acts like the user is unauthorized with HTTP 401.

What am I missing here? Why it works only without UMA?

I have looked at these (app-authz-uma-photoz, devconf2019-authz) examples and haven't noticed what I am missing.

Except they are actually creating some resources for users from the Java app, I'm not. But I guess it shouldn't matter if I'm protecting user created resources or single "pre-made" URL, right? It should depend only on correct permissions and since they evaluate to PERMIT so I don't see why this doesn't work.

And one more question. Isn't this UMA thing overkill for just "user can access his own, admin can access everything" case when there will never be any sharing between users? I was thinking about some simpler way that could work without creating user resources in Keycloak but I couldn't think of anything, I believe I still need to have connected user ID with some resource ID to make this working.

TheSpixxyQ
  • 744
  • 1
  • 6
  • 16

2 Answers2

2

The Keycloak adapters for Spring were deprecated early 2022. Don't use it. It is not even compatible with Spring Boot 3.

I detailed alternate solutions for configuring a resource server in the answer to this question: Use Keycloak Spring Adapter with Spring Boot 3, but for samples of advanced access control I suggest you refer to my tutorials on that subject.

In the case were you absolutely want to use the authorization service, you'll have to manually call the REST endpoint with WebClient, @FeignClient or whatever.

I don't like Keycloak authorization service (aka policy enforcer) for the following reasons:

  • it is quite inefficient: the resource server has to send a request to the authorization server (Keycloak) for each and every request it processes to fetch access decision
  • access control defined that way it pretty difficult (impossible?) to unit test
  • it is very Keycloak adherent (it is not standard and it's very unlikely that you find the exact same feature accepting the same configuration, if you are ever forced to use another identity provider)
  • it is very easy to get configuration discrepancies between environments. Access control "bugs" on a certain environment can be a nightmare to spot and fix, which makes it, in my opinion, a very high security risk for the app: how to ensure that, at any point time, nobody inserted an erroneous or malicious authorization rule in prod?

If, instead, you write method security expressions like

@PreAtuhorize("hasAutority('ADMIN') or #weatherForecast.authorUsername == #auth.tokenAttributes['preferred_username']")
@GetMapping("/weatherforecast/{forecastId}")
public WeatherForecastDto getForecast(@PathVariable("forecastId") WeatherForecastEntity weatherForecast, JwtAuthenticationToken auth) {
    ...
}

Then:

  • the authorization server is queried by the client for access tokens issuance (once in quite a few requests, like with authorization service), but you save all access control requests from the resource server
  • you can write unit test with mocked JwtAuthenticationToken instances to assert that access control behaves the way you expect
  • authorization rules are immutable and shipped with the application (no one can alter it in prod)
  • it is portable to any authorization-server on which you can control access token private claims (most allow it)
ch4mp
  • 6,622
  • 6
  • 29
  • 49
1

Let's check your KeycloakSecurityContext in debug or your Catalina session, maybe it will be null and return 403. Just add enforcer config into spring boot profile with prefix: keycloak for init default context of KeycloakSecurityContext, Spring boot will be able to load Enforcer config from keycloak server.

nobjta_9x_tq
  • 1,205
  • 14
  • 16
  • Keycloak adapters for Spring were [deprecated in early 2022](https://github.com/keycloak/keycloak/discussions/10187). Just don't use it, even more in a new project like this one. – ch4mp Mar 06 '23 at 04:40
  • @ch4mp you mean we shouldn't use Keycloak anymore? With Spring security, we must to code for ourselves => it will take more cost. – nobjta_9x_tq Mar 06 '23 at 05:03
  • 1
    You use Keycloak authorization server but not the adapters for spring. `spring-boot-starter-oauth2-resource-server` makes things easy enough and my starters (linked in my answer) allow to configure a resource-server with 0 Java code (this is less than deprecated Keycloak adapters). – ch4mp Mar 06 '23 at 06:19
  • does policy enforcer work? I will take a look. – nobjta_9x_tq Mar 06 '23 at 06:41
  • Read my answer below: it can't work out of the box because this is nothing standard (it is Keycloak proprietary stuff and both starters mentioned are vendor agnostic). You'll have to code the REST query to the policy enforcer by yourself. – ch4mp Mar 06 '23 at 06:55