[JAVA-27759] Fix Spring 3.1.x and Keycloak 22.x OAuth2 Tutorial (#15618)

This commit is contained in:
Amit Pandey 2024-01-22 12:42:00 +05:30 committed by GitHub
parent a09f1a89f1
commit 22a1ca2978
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 67 additions and 31 deletions

View File

@ -107,6 +107,7 @@
<jaxb-runtime.version>4.0.0</jaxb-runtime.version> <jaxb-runtime.version>4.0.0</jaxb-runtime.version>
<wsdl4j.version>1.6.3</wsdl4j.version> <wsdl4j.version>1.6.3</wsdl4j.version>
<jaxb2-maven-plugin.version>3.1.0</jaxb2-maven-plugin.version> <jaxb2-maven-plugin.version>3.1.0</jaxb2-maven-plugin.version>
<maven.compiler.release>17</maven.compiler.release>
</properties> </properties>
</project> </project>

View File

@ -1,15 +1,21 @@
package com.baeldung.keycloak; package com.baeldung.keycloak;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer; import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; 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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; 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.SessionRegistryImpl; 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.SecurityFilterChain;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy; import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
@ -19,6 +25,10 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@EnableWebSecurity @EnableWebSecurity
class SecurityConfig { class SecurityConfig {
private static final String GROUPS = "groups";
private static final String REALM_ACCESS_CLAIM = "realm_access";
private static final String ROLES_CLAIM = "roles";
private final KeycloakLogoutHandler keycloakLogoutHandler; private final KeycloakLogoutHandler keycloakLogoutHandler;
SecurityConfig(KeycloakLogoutHandler keycloakLogoutHandler) { SecurityConfig(KeycloakLogoutHandler keycloakLogoutHandler) {
@ -30,38 +40,63 @@ class SecurityConfig {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl()); return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
} }
@Order(1)
@Bean @Bean
public SecurityFilterChain clientFilterChain(HttpSecurity http) throws Exception { public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests() http.authorizeHttpRequests(auth -> auth
.requestMatchers(new AntPathRequestMatcher("/customers*"))
.hasRole("user")
.requestMatchers(new AntPathRequestMatcher("/")) .requestMatchers(new AntPathRequestMatcher("/"))
.permitAll() .permitAll()
.anyRequest() .anyRequest()
.authenticated(); .authenticated());
http.oauth2Login()
.and()
.logout()
.addLogoutHandler(keycloakLogoutHandler)
.logoutSuccessUrl("/");
return http.build();
}
@Order(2)
@Bean
public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.requestMatchers(new AntPathRequestMatcher("/customers*"))
.hasRole("USER")
.anyRequest()
.authenticated();
http.oauth2ResourceServer((oauth2) -> oauth2 http.oauth2ResourceServer((oauth2) -> oauth2
.jwt(Customizer.withDefaults())); .jwt(Customizer.withDefaults()));
http.oauth2Login(Customizer.withDefaults())
.logout(logout -> logout.addLogoutHandler(keycloakLogoutHandler).logoutSuccessUrl("/"));
return http.build(); return http.build();
} }
@Bean @Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception { public GrantedAuthoritiesMapper userAuthoritiesMapperForKeycloak() {
return http.getSharedObject(AuthenticationManagerBuilder.class) return authorities -> {
.build(); 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();
// Tokens can be configured to return roles under
// Groups or REALM ACCESS hence have to check both
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 = (Collection<String>) 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());
} }
} }

View File

@ -73,7 +73,7 @@ class KeycloakSoapLiveTest {
ResponseEntity<String> responseEntity = restTemplate.postForEntity("http://localhost:" + port + "/ws/api/v1/", request, String.class); ResponseEntity<String> responseEntity = restTemplate.postForEntity("http://localhost:" + port + "/ws/api/v1/", request, String.class);
assertThat(responseEntity).isNotNull(); assertThat(responseEntity).isNotNull();
assertThat(responseEntity.getStatusCodeValue()).isEqualTo(HttpStatus.OK.value()); assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK.value());
assertThat(responseEntity.getBody()).isNotBlank(); assertThat(responseEntity.getBody()).isNotBlank();
assertThat(responseEntity.getBody()).containsIgnoringCase(":id>1</"); assertThat(responseEntity.getBody()).containsIgnoringCase(":id>1</");
} }
@ -92,7 +92,7 @@ class KeycloakSoapLiveTest {
HttpEntity<String> request = new HttpEntity<>(Utility.getGetProductDetailsRequest(), headers); HttpEntity<String> request = new HttpEntity<>(Utility.getGetProductDetailsRequest(), headers);
ResponseEntity<String> responseEntity = restTemplate.postForEntity("http://localhost:" + port + "/ws/api/v1/", request, String.class); ResponseEntity<String> responseEntity = restTemplate.postForEntity("http://localhost:" + port + "/ws/api/v1/", request, String.class);
assertThat(responseEntity).isNotNull(); assertThat(responseEntity).isNotNull();
assertThat(responseEntity.getStatusCodeValue()).isEqualTo(HttpStatus.UNAUTHORIZED.value()); assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED.value());
assertThat(responseEntity.getBody()).isBlank(); assertThat(responseEntity.getBody()).isBlank();
} }
@ -110,7 +110,7 @@ class KeycloakSoapLiveTest {
ResponseEntity<String> responseEntity = restTemplate.postForEntity("http://localhost:" + port + "/ws/api/v1/", request, String.class); ResponseEntity<String> responseEntity = restTemplate.postForEntity("http://localhost:" + port + "/ws/api/v1/", request, String.class);
assertThat(responseEntity).isNotNull(); assertThat(responseEntity).isNotNull();
assertThat(responseEntity.getStatusCodeValue()).isEqualTo(HttpStatus.OK.value()); assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK.value());
assertThat(responseEntity.getBody()).isNotBlank(); assertThat(responseEntity.getBody()).isNotBlank();
assertThat(responseEntity.getBody()).containsIgnoringCase("Deleted the product with the id"); assertThat(responseEntity.getBody()).containsIgnoringCase("Deleted the product with the id");
} }
@ -130,7 +130,7 @@ class KeycloakSoapLiveTest {
ResponseEntity<String> responseEntity = restTemplate.postForEntity("http://localhost:" + port + "/ws/api/v1/", request, String.class); ResponseEntity<String> responseEntity = restTemplate.postForEntity("http://localhost:" + port + "/ws/api/v1/", request, String.class);
assertThat(responseEntity).isNotNull(); assertThat(responseEntity).isNotNull();
assertThat(responseEntity.getStatusCodeValue()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.value()); assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.value());
assertThat(responseEntity.getBody()).isNotBlank(); assertThat(responseEntity.getBody()).isNotBlank();
assertThat(responseEntity.getBody()).containsIgnoringCase("Access is denied"); assertThat(responseEntity.getBody()).containsIgnoringCase("Access is denied");
} }