I have been following this tutorial: https://www.baeldung.com/spring-boot-keycloak to set up my Spring app with Keycloak. The authentication works normally, but when I want to add role-based authentication, for example
.requestMatchers(new AntPathRequestMatcher("/api/v1/user/**")).hasRole("user")
I always get the following error
Bearer error="insufficient_scope", error_description="The request requires higher privileges than provided by the access token.", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"
I have checked the authorites using
SecurityContextHolder.getContext().getAuthentication().getAuthorities();
and it only has ones that have the SCOPE_
prefix.
I have also checked the token and it returns this correctly
"realm_access": {
"roles": [
"offline_access",
"uma_authorization",
"default-master-realm",
"user"
]
},
besides the other fields.
This is my KeycloakConfig
class:
package com.motus.core.shared.config.security;
import com.motus.auth.constants.Authority;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
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.session.HttpSessionEventPublisher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
@EnableWebSecurity
@Configuration
public class KeycloakConfig {
private static final String GROUPS = "groups";
private static final String REALM_ACCESS_CLAIM = "realm_access";
private static final String ROLES_CLAIM = "roles";
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Bean
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(sessionRegistry());
}
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, KeycloakLogoutHandler keycloakLogoutHandler) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers(new AntPathRequestMatcher("/api/v1/public/**")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/api/v1/auth/**")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/api/v1/admin/**")).hasRole("admin")
.requestMatchers(new AntPathRequestMatcher("/api/v1/user/**")).hasRole("user")
.anyRequest().authenticated()
);
http.oauth2ResourceServer(oauth2 -> oauth2
.jwt(Customizer.withDefaults()));
http.oauth2Login(Customizer.withDefaults())
.logout(logout -> logout.addLogoutHandler(keycloakLogoutHandler).logoutSuccessUrl("/"));
http.csrf(AbstractHttpConfigurer::disable);
http.cors(Customizer.withDefaults());
return http.build();
}
@Bean
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 if (userInfo.hasClaim(GROUPS)) {
Collection<String> roles = userInfo.getClaim(
GROUPS);
mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
}
} else {
var oauth2UserAuthority = (OAuth2UserAuthority) authority;
Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
if (userAttributes.containsKey(REALM_ACCESS_CLAIM)) {
Map<String, Object> realmAccess = (Map<String, Object>) userAttributes.get(
REALM_ACCESS_CLAIM);
Collection<String> 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());
}
}
It looks like the GrantedAuthoritesMapper is not being invoked, do you know why?
Here is what I added in 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-oauth2-resource-server</artifactId>
</dependency>
and here is what I added in my application.properties:
spring.security.oauth2.client.registration.keycloak.client-id=${KEYCLOAK_CLIENT_ID:test-backend}
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=${KEYCLOAK_AUTHORIZATION_GRANT_TYPE:password}
spring.security.oauth2.client.registration.keycloak.scope=${KEYCLOAK_SCOPE:openid}
spring.security.oauth2.client.provider.keycloak.issuer-uri=${KEYCLOAK_ISSUER_URI:http://localhost:8080/realms/master}
spring.security.oauth2.client.provider.keycloak.user-name-attribute=${KEYCLOAK_USER_NAME_ATTRIBUTE:username}
spring.security.oauth2.resourceserver.jwt.issuer-uri=${KEYCLOAK_RESOURCE_SERVER_JWT_ISSUER_URI:http://localhost:8080/realms/master}
I obtain the token using http://localhost:8080/realms/master/protocol/openid-connect/token
Then I send the request to my app with the obtained token, and I always get the error mentioned above. Does anyone know how can I fix this?
hasRole('user')
requires the ROLE_user
authorityEither:
hasAuthority('user')
ROLE_
prefix when mapping Keycloak roles to Spring Security authoritiesoauth2ResourceServer
and oauth2Login
in the same security filter-chainRemember that an OAuth2 client consumes resources served by resource servers (these are different actors). In micro-services architectures, it is frequent to have applications with both roles, but for for different resources (which should be secured with different Security(Web)FilterChain
beans).
Spring Boot provides different starters for a reason. I know that the Baeldung tutorial, as well as many others from authors who copy from each other without understanding what they are doing, are using oauth2ResourceServer
and oauth2Login
in the same security filter-chain, but this is nonsense. Let's get a closer look at the security configuration for the two:
oauth2ResourceServer
is configuring an OAuth2 resource server which:
Bearer
token in Authorization
header401 Unauthorized
when the Authorization
header is missing or invalidConverter<Jwt, ? extends AbstractAuthenticationToken>
(if using JWT decoding, or customizing the introspector for opaque tokens)oauth2Login
is configuring an OAuth2 client with authorization code flow which:
Bearer
token in Authorization
header)302 Redirect to login
to requests without a valid sessionGrantedAuthoritiesMapper
(or with a custom OAuth2UserService)These requirements are too different to stand in the same Security(Web)FilterChain
bean. Also, note from the above that authorities are not mapped the same way on oauth2ResourceServer
with JWT decoder, oauth2ResourceServer
with (opaque) token introspector, and OAuth2 clients with oauth2Login
.
You should probably read my tutorials intro to get some more OAuth2 background.
The additional starter I host in the same repo can help you configure your Spring applications for Keycloak with just properties (these Spring apps being mainly OAuth2 clients with oauth2Login
or OAuth2 resource servers)