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.
- user Then I created a user and added to him the roles that I created earlier 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
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.
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:
- Configure keycloak to put role inside the id token.
- Define a mapper to map the role inside the id token to the spring security role.
Let's do it:
- 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"
Add mapper -> Realm Roles
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.
You can go to Client scope -> Evaluate and verify that the role is present in the ID token.
- 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 !