Home > Back-end >  Keycloak with Spring Boot based on roles does not work, which were assigned to the user in keycloak
Keycloak with Spring Boot based on roles does not work, which were assigned to the user in keycloak

Time:12-22

I have configured the use of Keycloak without using a spring adapter. Since it is deprecated. I created in the console of Keycloak: a REALM, a user, and add roles for the user.

enter image description here

enter image description here

  • user Then I created a user and added to him the roles that I created earlier here.

enter image description here

enter image description here

  • docker
....
  keycloak:
    container_name: blog
    depends_on:
      keycloakdb:
        condition: service_healthy
    environment:
      DB_DATABASE: ${POSTGRESQL_DB}
      DB_USER: ${POSTGRESQL_USER}
      DB_PASSWORD: ${POSTGRESQL_PASS}
      KEYCLOAK_USER: ${KEYCLOAK_USER}
      KEYCLOAK_PASSWORD: ${KEYCLOAK_PASSWORD}
      DB_VENDOR: ${DB_VENDOR}
      DB_ADDR: ${DB_ADDR}
      DEBUG_PORT: ${DEBUG_PORT}
      DB_PORT: ${DB_PORT}
      TZ: ${TZ}
      DEBUG: ${DEBUG}
    image: jboss/keycloak:latest
.....

here is the application configuration

  • pom.xml
  <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.0.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.guide</groupId>
    <artifactId>keycloak-postgres-quick-guide</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>keycloak-postgres-quick-guide</name>
    <description>keycloak-postgres-quick-guide</description>
    <properties>
        <java.version>17</java.version>
        <testcontainers.version>1.17.6</testcontainers.version>
        <snakeyaml.version>1.33</snakeyaml.version>
        <keycloak.version>20.0.2</keycloak.version>
    </properties>
    <dependencies>
....
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
...
  
    </dependencies>
    <dependencyManagement>
        <dependencies>

            <dependency>
                <groupId>org.keycloak</groupId>
                <artifactId>keycloak-spring-boot-starter</artifactId>
                <version>${keycloak.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

        </dependencies>
    </dependencyManagement>
  • security
@Configuration
@EnableWebSecurity(debug = true)
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final KeycloakLogoutHandler keycloakLogoutHandler;

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

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .requestMatchers("/customers*", "/users*")
                .hasRole("READ")
                .anyRequest()
                .permitAll();
        http.oauth2Login()
                .and()
                .logout()
                .addLogoutHandler(keycloakLogoutHandler)
                .logoutSuccessUrl("/");
        return http.build();
    }
}
  • controller
    @GetMapping(path = "/customers")
    public String customers(Principal principal, Model model) {

        addCustomers();

        Iterable<Customer> customers = customerRepository.findAll();
        model.addAttribute("customers", customers);
        model.addAttribute("username", principal.getName());

        return "customers";
    }
  • application.yml
spring:

  security:
    oauth2:
      client:
        provider:
          keycloak:
            issuer-uri: http://localhost:28080/auth/realms/SpringBootKeycloak
            user-name-attribute: preferred_username
        registration:
          keycloak:
            authorization-grant-type: authorization_code
            client-id: loggin-app
            scope: openid

enter image description here enter image description here

however, when trying to access a protected resource, I get an error :

Sending OAuth2AuthenticationToken [Principal=Name: [user-spring-app], Granted Authorities: [[OIDC_USER, SCOPE_email, SCOPE_openid, SCOPE_profile]], User Attributes: [{at_hash=yGjLEXdKSgOC3J8_QfLyrw, sub=26e4c1c7-cb02-4628-bae9-7a370b53c067, email_verified=false, iss=http://localhost:28080/auth/realms/SpringBootKeycloak, typ=ID, preferred_username=user-spring-app, nonce=yzqik3W-aQtCjP6nQGt-N2CbnyI7O7smrt6mLOZNXY8, sid=07dbaa6a-826c-4139-a956-62a477f6eefe, aud=[loggin-app], acr=1, azp=loggin-app, auth_time=2022-12-21T08:22:10Z, exp=2022-12-21T08:27:10Z, session_state=07dbaa6a-826c-4139-a956-62a477f6eefe, iat=2022-12-21T08:22:10Z, jti=93f19918-bc4a-4435-bbcd-578f7ca2ed36}], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=A0B52BA34017AD4C0183CA1DD48420B8], Granted Authorities=[OIDC_USER, SCOPE_email, SCOPE_openid, SCOPE_profile]] to access denied handler since access is denied org.springframework.security.access.AccessDeniedException: Access Denied

I am not observing the roles that I have assigned to the user

Principal=Name: [user-spring-app], Granted Authorities: [[OIDC_USER, SCOPE_email, SCOPE_openid, SCOPE_profile]],

I am trying to understand how keycloak works, but so far there is only fragmentary information. The example that I found is outdated (the spring-boot-Keycloak-adapter deprecated now). In addition, I have seen different approaches, someone uses hasAuthority(), and someone uses hasRole(), however, I did not understand how it works in relation to Keycloak.

enter image description here enter image description here

enter image description here Does anyone have an understanding of how to fix this and explain the principle of such work?

CodePudding user response:

I think you have to:

  1. Configure keycloak to put role inside the id token.
  2. Define a mapper to map the role inside the id token to the spring security role.

Let's do it:

  1. Configure keycloak: My version is 20.x, my client is acme

Go to your keycloak realm -> Client -> Open your client -> Tab "Client scopes" -> In the table, click on the "acme-dedicated"

enter image description here

Add mapper -> Realm Roles

enter image description here

Uncheck and check Add to ID token: the checkbox may be checked by default but it isn't activated, we have to manually uncheck then check it again. Once done, click one Save.

enter image description here

You can go to Client scope -> Evaluate and verify that the role is present in the ID token.

  1. OK, now let configure spring boot to map keycloak role to spring security role.

To do it, add the following code to your class SecurityConfig:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private static final String REALM_ACCESS_CLAIM = "realm_access";
    private static final String ROLES_CLAIM = "roles";

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

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.oauth2Login(Customizer.withDefaults());
        ...
        return http.build();
    }

    @Bean
    @SuppressWarnings("unchecked")
    public GrantedAuthoritiesMapper userAuthoritiesMapperForKeycloak() {
        return authorities -> {
            Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
            var authority = authorities.iterator().next();
            boolean isOidc = authority instanceof OidcUserAuthority;

            if (isOidc) {
                var oidcUserAuthority = (OidcUserAuthority) authority;
                var userInfo = oidcUserAuthority.getUserInfo();

                if (userInfo.hasClaim(REALM_ACCESS_CLAIM)) {
                    var realmAccess = userInfo.getClaimAsMap(REALM_ACCESS_CLAIM);
                    var roles = (Collection<String>) realmAccess.get(ROLES_CLAIM);
                mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
                }
            } else {
                var oauth2UserAuthority = (OAuth2UserAuthority) authority;
                Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();

                if (userAttributes.containsKey(REALM_ACCESS_CLAIM)) {
                    var realmAccess = (Map<String, Object>) userAttributes.get(REALM_ACCESS_CLAIM);
                    var roles = (Collection<String>) realmAccess.get(ROLES_CLAIM);
                    mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
                }
            }
            return mappedAuthorities;
        };
    }

    Collection<GrantedAuthority> generateAuthoritiesFromClaim(Collection<String> roles) {
        return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_"   role)).collect(Collectors.toList());
        }
    }

FYI: I don't use keycloak java adapter because it will be deprecated soon. I use oauth2-client with spring security. This is my pom.xml:

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

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

Good lucks !

  • Related