0

I want to give access based on the realm roles. I am using Keycloak and Spring Boot 3 (Spring Security 6).

I tried hasRole()/hasAnyRole/hasAuthority/hasAnyAuthority with the desired role name. The letter case is the same.

By default use-resource-role-mappings is false("If false, it will look at the realm level for user role mappings").

I'd like to set this in java configuration file, as I would find it very time consuming to add on each controller @PreAuthorize annotation.

I keep getting 403 as a response. What am I doing wrong? Error:

Retrieved SecurityContextImpl [Authentication=OAuth2AuthenticationToken [Principal=Name: [firstname.lastname], Granted Authorities: [[OIDC_USER, SCOPE_ProjectNameClientScope, SCOPE_email, SCOPE_openid, SCOPE_profile]], 
User Attributes: [{at_hash=hash-hash-hash, sub=sub-sub-sub-sub-sub, email_verified=true, iss=https://keycloak.domain.ac.at:port/realms/ProjectNameRealm, groups=[/pn_administratoren], 
Roles=[cafeteria, obw, ROLE_obw, courseplanning], typ=ID, preferred_username=firstname.lastname, given_name=firstname, nonce=nonce-y-nonce-nonce, sid=sid-sid-sid-sid-sid, aud=[po-ms], acr=1, persId=number, azp=po-ms, auth_time=2023-01-18T13:36:38Z, name=firstname lastname, exp=2023-01-18T13:41:38Z, 
session_state=sss-sss-sss-ss-sss, family_name=lastname, iat=2023-01-18T13:36:38Z, email=firstname.lastname@domain.ac.at, jti=jti-jti-jti-jti-jti}], 
Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=xxx.xxx.xx.xx, SessionId=sesId], 
Granted Authorities=[OIDC_USER, SCOPE_ProjectNameClientScope, SCOPE_email, SCOPE_openid, SCOPE_profile]]]

Here is my code:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestHandler;
import org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

@Configuration
@EnableWebSecurity
public class SpringSecurity2 {
    
    public interface Jwt2AuthoritiesConverter extends Converter<Jwt, Collection<? extends GrantedAuthority>> {
    }

    @SuppressWarnings("unchecked")
    @Bean
    public Jwt2AuthoritiesConverter authoritiesConverter() {
        // This is a converter for roles as embedded in the JWT by a Keycloak server
        // Roles are taken from both realm_access.roles & resource_access.{client}.roles
        return jwt -> {
            final var realmAccess = (Map<String, Object>) jwt.getClaims().getOrDefault("realm_access", Map.of());
            final var realmRoles = (Collection<String>) realmAccess.getOrDefault("roles", List.of());

            final var resourceAccess = (Map<String, Object>) jwt.getClaims().getOrDefault("resource_access", Map.of());
            // We assume here you have "spring-addons-confidential" and "spring-addons-public" clients configured with "client roles" mapper in Keycloak
            final var confidentialClientAccess = (Map<String, Object>) resourceAccess.getOrDefault("spring-addons-confidential", Map.of());
            final var confidentialClientRoles = (Collection<String>) confidentialClientAccess.getOrDefault("roles", List.of());
            final var publicClientAccess = (Map<String, Object>) resourceAccess.getOrDefault("spring-addons-public", Map.of());
            final var publicClientRoles = (Collection<String>) publicClientAccess.getOrDefault("roles", List.of());

            return Stream.concat(realmRoles.stream(), Stream.concat(confidentialClientRoles.stream(), publicClientRoles.stream()))
                    .map(SimpleGrantedAuthority::new).toList();
        };
    }

    public interface Jwt2AuthenticationConverter extends Converter<Jwt, AbstractAuthenticationToken> {
    }

    @Bean
    public Jwt2AuthenticationConverter authenticationConverter(Jwt2AuthoritiesConverter authoritiesConverter) {
        return jwt -> new JwtAuthenticationToken(jwt, authoritiesConverter.convert(jwt));
    }

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


    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, Converter<Jwt, AbstractAuthenticationToken> authenticationConverter, KeycloakLogoutHandler keycloakLogoutHandler) throws Exception {

        CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
        XorCsrfTokenRequestAttributeHandler delegate = new XorCsrfTokenRequestAttributeHandler();
        delegate.setCsrfRequestAttributeName("_csrf");

        CsrfTokenRequestHandler requestHandler = delegate::handle;

        http.authorizeHttpRequests(auth -> {
            auth.requestMatchers("/firstpath/**", "/secondpath/**").permitAll();
            auth.requestMatchers("/thirdpath/**").hasAnyRole("obw");
            auth.anyRequest().authenticated();
        });

        http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(authenticationConverter);
        http.oauth2Login()
                .and()
                .logout()
                .addLogoutHandler(keycloakLogoutHandler)
                .logoutSuccessUrl("/");

        http.csrf(csrf -> csrf
                .csrfTokenRepository(tokenRepository)
                .csrfTokenRequestHandler(requestHandler));

        return http.build();
    }
    
}

enter image description here

The logged in user's roles The logged in user's roles

The same with ROLE_obw...

And this is part of the access token: enter image description here

Octavia
  • 198
  • 1
  • 13
  • I am surprised you have oauth2Login on a resource-server. Is your app an API or a server-side rendered UI (with Thymeleaf or whatever)? – ch4mp Jan 17 '23 at 16:56
  • I sorry, I'm not so experimented , and idk what "API or a server-side rendered UI" means. I have access just on the backend, there is no Thymeleaf, but from what I heard, it is a REST-API app.... – Octavia Jan 18 '23 at 14:00
  • You can still Google it... If your Spring controllers are `@Controller` with `@RespnseBody` (or `@RestController` which is a shorthand for it), then your app is a resource-server. If `@Controller` methods return template names (`String`) then it is serving UI and app is client. – ch4mp Jan 18 '23 at 18:57
  • Ok, thank you for the explanation! It is a resource-server – Octavia Jan 19 '23 at 07:34

2 Answers2

4

Login (and logout) are handled by OAuth2 clients, not resource-servers (REST APIs). Requests to resource-server protected resources should have an access-token.

Remove login and logout conf from your resource-server security filter-chain (you might also make it session-less and disable CSRF) and send requests with Bearer access-token in Authorization header.

If your app also serves server-side rendered UI, define a second SecurityFilterChain bean for client configuration (with login, logout, sessions and CSRF protection enabled).

Details in

ch4mp
  • 6,622
  • 6
  • 29
  • 49
  • I updated my code, but still, I have no success with the roles. I am using Keycloak for authentication, but in an endpoint (secondpath, with permitAll()), I receive the credentials for a user and based on them I ask an access token from Keycloak. So for that endpoint I need CSRF protection. – Octavia Jan 18 '23 at 13:57
  • Read carefully the linked answer, and get more background about OAuth2 (more specifically with flows and actors). – ch4mp Jan 18 '23 at 19:00
  • More specifically: CSRF protection must be activated when sessions are used (not depending on requests content). Also, user credentials should be given to **authorization-server only** and neither to client nor resource-server (I supect you intend to exchange it between those two). Last, again, redirection to authorization-server for login has nothing to do in resource-servers security filter-chain: it is client job. – ch4mp Jan 19 '23 at 13:30
  • We've been trying around with the client (login) access, and while there is an access token involved in the process (containing the realm roles), another query is performed retrieving the user token, which does not have those anymore - so as a result, you get a `OidcUserAuthority` object without the realm attributes. We also looked around the Spring code and it's not trivial to re-establish them in the resulting authority list :( – daniu Jun 08 '23 at 14:24
  • Read the linked answer again: queries to clients are not secured with access tokens, it is secured with session. `OidcUserAuthority` is part of client security, it's details is built from user-info endpoint (or ID token), not from access token => Keycloak must be configured to put roles in user-info and ID tokens (but it should by default), and Spring must be configured with client authorities mapping. Follow the links... – ch4mp Jun 08 '23 at 16:40
  • @ch4mp yeah I was referring to the links - I do assume our Keycloak isn't actually configured to do what we want to do, but it didn't matter before when we were using the deprecated KeycloakAdapter. We did see an AccessToken while establishing the auth_code session, or we're still doing something wrong. Anyway, thanks a lot for your explanations, these were the best we could find about this topic overall. – daniu Jun 09 '23 at 07:21
1

By default there is only scope based mapping provided by Spring security for handling access via JWT token. For Authorities/Roles need a custom mapping solution to extract from JWT token and put into security context.

There is a nice article on same which explains this.

Map Authorities from JWT

Saurabh Singh
  • 198
  • 1
  • 7
  • Thank you, I tried to, but still not working... I updated my code in the question. – Octavia Jan 17 '23 at 13:53
  • This role mapping is detailed in the other answer I linked. It is also much easier (no java code, just configuration properties) in the first solution I detail there (second option is basically the same as the Baeldung article you link). Plus his question already contains authorities mapping... – ch4mp Feb 15 '23 at 14:49