Protect individual resources from Spring Boot via Keycloak

Viewed 1490

I've got a very simple Spring Boot application with resources at /repositories and /persons.

Here is my build.gradle file.

plugins {
    id 'org.springframework.boot' version '2.4.0'
    id 'io.spring.dependency-management' version '1.0.10.RELEASE'
    id 'java'
}

// use java 11 until keycloak is fixed
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencyManagement {
    imports {
        mavenBom "org.keycloak.bom:keycloak-adapter-bom:12.0.1"
    }
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.keycloak:keycloak-spring-boot-starter'

    implementation 'org.flywaydb:flyway-core'
    runtime 'org.postgresql:postgresql'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

...
...

Here is my SecurityConfig.java file.

@KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {

  @Autowired
  public void configureGlobal(AuthenticationManagerBuilder auth) {
    var keycloakAuthenticationProvider = keycloakAuthenticationProvider();
    keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
    auth.authenticationProvider(keycloakAuthenticationProvider);
  }

  @Bean
  @Override
  protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
    return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    super.configure(http);
    http.authorizeRequests()
        .antMatchers("/persons*")
        .hasRole("user")
        .anyRequest()
        .permitAll();
  }

  @Bean
  public KeycloakConfigResolver keycloakConfigResolver() {
    return new KeycloakSpringBootConfigResolver();
  }
}

Here is my application.yaml file.

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/postgres
    username: john
    password: john

keycloak:
  auth-server-url: http://localhost:8081/auth/
  realm: myrealm
  resource: myclient
  credentials:
    secret: 45d43bd6-5ab9-476c-83c8-67bd203a78ee

It's all on my local machine and Keycloak and Postgres are started via docker compose.


version: "3.1"

volumes:
  postgres_data:
      driver: local

services:
  db:
    image: "postgres:13.1"
    ports:
      - "5432:5432"
    environment:
      POSTGRES_DB: postgres
      POSTGRES_USER: john
      POSTGRES_PASSWORD: john
  postgres:
    image: "postgres:13.1"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: keycloak
      POSTGRES_PASSWORD: password
  keycloak:
    image: quay.io/keycloak/keycloak:12.0.1
    environment:
      DB_VENDOR: POSTGRES
      DB_ADDR: postgres
      DB_DATABASE: keycloak
      DB_USER: keycloak
      DB_SCHEMA: public
      DB_PASSWORD: password
      KEYCLOAK_USER: admin
      KEYCLOAK_PASSWORD: Pa55w0rd
    ports:
      - 8081:8080
    depends_on:
      - postgres

I've got the users zemirco and one in Keycloak and they both have the user role. It all works very nicely and the route /persons is protected whereas /repositories is open for all.

Here is my problem! I'd like to secure individual resources, e.g. /person/1. I cannot make it work :( I've been trying for days now but no luck. Here are my keycloak settings.

Here is my policy.

policy

Here is the resource.

resource

Here is the permission.

permission

It all works during the evaluating at Keycloak. However it does not work directly at my Spring Boot application. I can still access /person/1 as zemirco although user one should only have access.

I'm a bit lost at the moment. Do you have any ideas?

Thank you very much!


Edit Jan 10 2021

Thank you all for your answers but I still think it's doable without a lot of extra effort on my side (the application side). I'm especially looking at policy enforcers https://www.keycloak.org/docs/latest/authorization_services/#_enforcer_overview.

A PEP is responsible for enforcing access decisions from the Keycloak server where these decisions are taken by evaluating the policies associated with a protected resource. It acts as a filter or interceptor in your application in order to check whether or not a particular request to a protected resource can be fulfilled based on the permissions granted by these decisions.

This sound exactly like what I'm trying to do. Unfortunately I cannot make it work. I just think my configuration is false or maybe incomplete. I will add a bounty to the question.

3 Answers

It finally works :) I'm happy!

Here is what I did.

First of all I used the keycloak.json file from my Keycloak client installation. At the beginning I tried to put all configuration into my application.yaml. The keycloak.json file must be located at src/main/webapp/WEB-INF/keycloak.json. When you're using the JSON file instead of the YAML file you can remove the following from your SecurityConfig.

public KeycloakConfigResolver keycloakConfigResolver() {
  return new KeycloakSpringBootConfigResolver();
}

It's actually quite nice. You can simply copy&paste the content from Keycloak into the file and you can be sure that everything is right. Another problem I saw is that the config in keycloak.json is called policy-enforcer compared to policy-enforcer-config in application.yaml. That's quite annoying because I didn't realize it at the beginning. Here is config.

{
  "realm": "myrealm",
  "auth-server-url": "http://localhost:8081/auth/",
  "ssl-required": "external",
  "resource": "myclient",
  "verify-token-audience": true,
  "credentials": {
    "secret": "45d43bd6-5ab9-476c-83c8-67bd203a78ee"
  },
  "confidential-port": 0,
  "policy-enforcer": {}
}

The "policy-enforcer": {} is the most important part. Here we enable the default settings and simply say that Keycloak should authorize all requests to our resources.

Then everything seemed to work. I was able to gain access to a protected resource only with the right user. However after I logged out and tried to access the resource again I was still able to do it. Here I learned that my ant matchers were wrong. This is the right configuration.

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    super.configure(http);
    http.authorizeRequests()
        .antMatchers("/persons/**") // changed from .antMatchers("/persons*")
        .hasRole("user")
        .anyRequest()
        .permitAll()
        .and()
        // this is just to have a convenient GET /logout method. DO NOT USE IN PRODUCTION
        .logout()
        .logoutRequestMatcher(new AntPathRequestMatcher("/logout"));
  }

Now everything works and my resources/policies/permissions from Keycloak are automatically applied in my Spring Boot application.

Keycloak is known for having the worst policy/access tools in Access management solutions, however, the problem you described seems a bit weird.

Keycloak is an Access management tool, from a policy point of view its provides decisions, it doesn't enforce them, and thus since the policy evaluation works as expected, all you need to do is make an api call to the evaluation enpoints from your spring app, if it's a 200 then the users would access, if not the page is not shown.

An alternative solutions would be to use a premade policy enforcement point, apache/nginx spring all have thier custon policy enforcement point.

Some usefull links:

how-to-use-keycloak-policy-enforcer-with-spring-boot-application

keycloak-enforcement-filter

AuthZ in Keycloak is PITA and IMHO one shouldn't use it. There are better solutions for that (e.g. OPA). Just following the principle: "Don't do stupid things!"

Secondly, don't mix up AuthN and AuthZ. Just because there are some AuthZ policies configured in Keycloak, this doesn't mean, that they are "automatically and magically" used. If you're using any adapter (no matter if Spring Security or some KC own adapters) for authentication, they're doing "just OIDC" and OIDC is about AuthN, not AuthZ.

If you want to adapt or use your AuthZ policies and rules in your application, you have to query them separately from authentication. Keycloak provides therefor the keycloak-authz-client, which is documented here: https://www.keycloak.org/docs/latest/authorization_services/#_service_client_api

Related