5

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?

skyho
  • 1,438
  • 2
  • 20
  • 47

1 Answers1

4

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 !

sn1987a
  • 201
  • 1
  • 6
  • I have for the created client, on its tab Client scopes - empty. (see the picture above). I don't understand what needs to be done ? I have a slightly different interface, I use the latest version of image: jboss/keycloak:latest . – skyho Dec 21 '22 at 12:56
  • I have selected one of the built-in protocol mappers - realm roles. I corrected it as you offer above. – skyho Dec 21 '22 at 13:26
  • Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=9E2258CCE4710BE568C8292CE9B42A5A], Granted Authorities=[ROLE_default-roles-springbootkeycloak, ROLE_offline_access, ROLE_ROLE_USER, ROLE_user, ROLE_uma_authorization, ROLE_read]] to access denied handler since access is denied – skyho Dec 21 '22 at 13:39
  • it turned out that the case of the letters in the name of the role, which was designated in, is important.hasRole("read"). thanks – skyho Dec 21 '22 at 13:40
  • 1
    This answer is the way to secure a Spring **client** application (no REST endpoint), as asked. To secure a resource-server (app exposing a REST API), see this other answer https://stackoverflow.com/a/74572732/619830 – ch4mp Dec 21 '22 at 15:16
  • Thanks. I followed the tutorial here: https://www.baeldung.com/spring-boot-keycloak and interestingly this is not described there so the whole role based auth was not working. – reencode Jun 07 '23 at 09:20