[JAVA-27759] Fix Spring 3.1.x and Keycloak 22.x OAuth2 Tutorial (#15618)
This commit is contained in:
parent
a09f1a89f1
commit
22a1ca2978
|
@ -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>
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue