Reputation: 242
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 protected], 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();
}
}
The same with ROLE_obw...
And this is part of the access token:
Upvotes: 0
Views: 2244
Reputation: 12744
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
Upvotes: 4
Reputation: 218
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.
Upvotes: 1