spring-boot 3.1 and spring-addons 7.1.10 (#14902)

This commit is contained in:
Jérôme Wacongne
2023-10-20 05:41:41 +02:00
committed by GitHub
parent fdca014527
commit 8f3b5ecc2b
13 changed files with 289 additions and 189 deletions
@@ -1,5 +1,7 @@
package com.baeldung;
import static org.springframework.security.config.Customizer.withDefaults;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -23,8 +25,10 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -43,56 +47,52 @@ public class ServletResourceServerApplication {
@EnableWebSecurity
static class SecurityConf {
@Bean
SecurityFilterChain filterChain(HttpSecurity http, Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter) throws Exception {
http.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(jwt -> new JwtAuthenticationToken(jwt, authoritiesConverter.convert(jwt)));
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf()
.disable();
http.exceptionHandling()
.authenticationEntryPoint((request, response, authException) -> {
response.addHeader(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"Restricted Content\"");
response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
});
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.oauth2ResourceServer(resourceServer -> resourceServer.jwt(withDefaults()));
http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.csrf(csrf -> csrf.disable());
http.exceptionHandling(eh -> eh.authenticationEntryPoint((request, response, authException) -> {
response.addHeader(HttpHeaders.WWW_AUTHENTICATE, "Bearer realm=\"Restricted Content\"");
response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
}));
http.authorizeHttpRequests()
.requestMatchers("/secured-route")
.hasRole("AUTHORIZED_PERSONNEL")
.anyRequest()
.authenticated();
// @formatter:off
http.authorizeHttpRequests(req -> req
.requestMatchers(new AntPathRequestMatcher("/secured-route")).hasRole("AUTHORIZED_PERSONNEL")
.anyRequest().authenticated());
// @formatter:on
return http.build();
}
static interface AuthoritiesConverter extends Converter<Jwt, Collection<GrantedAuthority>> {
static interface JwtAuthoritiesConverter extends Converter<Jwt, Collection<GrantedAuthority>> {
}
@Bean
AuthoritiesConverter realmRoles2AuthoritiesConverter() {
JwtAuthoritiesConverter realmRoles2AuthoritiesConverter() {
return (Jwt jwt) -> {
final var realmRoles = Optional.of(jwt.getClaimAsMap("realm_access"))
.orElse(Map.of());
final var realmRoles = Optional.of(jwt.getClaimAsMap("realm_access")).orElse(Map.of());
@SuppressWarnings("unchecked")
final var roles = (List<String>) realmRoles.getOrDefault("roles", List.of());
return roles.stream()
.map(SimpleGrantedAuthority::new)
.map(GrantedAuthority.class::cast)
.toList();
return roles.stream().map(SimpleGrantedAuthority::new).map(GrantedAuthority.class::cast).toList();
};
}
@Bean
JwtAuthenticationConverter authenticationConverter(Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter) {
final var authenticationConverter = new JwtAuthenticationConverter();
authenticationConverter.setPrincipalClaimName(StandardClaimNames.PREFERRED_USERNAME);
authenticationConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
return authenticationConverter;
}
}
@Service
public static class MessageService {
public String greet() {
final var who = (JwtAuthenticationToken) SecurityContextHolder.getContext()
.getAuthentication();
final var claims = who.getTokenAttributes();
return "Hello %s! You are granted with %s.".formatted(claims.getOrDefault(StandardClaimNames.PREFERRED_USERNAME, claims.get(StandardClaimNames.SUB)), who.getAuthorities());
final var who = (JwtAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
return "Hello %s! You are granted with %s.".formatted(who.getName(), who.getAuthorities());
}
@PreAuthorize("hasRole('AUTHORIZED_PERSONNEL')")
@@ -3,28 +3,49 @@ package com.baeldung;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.test.context.support.WithAnonymousUser;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import com.baeldung.ServletResourceServerApplication.MessageService;
import com.c4_soft.springaddons.security.oauth2.test.annotations.OpenIdClaims;
import com.c4_soft.springaddons.security.oauth2.test.annotations.WithMockJwtAuth;
import com.baeldung.ServletResourceServerApplication.SecurityConf;
import com.c4_soft.springaddons.security.oauth2.test.AuthenticationFactoriesTestConf;
import com.c4_soft.springaddons.security.oauth2.test.annotations.WithJwt;
import com.c4_soft.springaddons.security.oauth2.test.annotations.parameterized.ParameterizedAuthentication;
@Import({ MessageService.class })
@Import({ MessageService.class, SecurityConf.class })
@ImportAutoConfiguration(AuthenticationFactoriesTestConf.class)
@ExtendWith(SpringExtension.class)
@EnableMethodSecurity
@TestInstance(Lifecycle.PER_CLASS)
class MessageServiceUnitTest {
@Autowired
MessageService messageService;
@Autowired
WithJwt.AuthenticationFactory authFactory;
@MockBean
JwtDecoder jwtDecoder;
/*----------------------------------------------------------------------------*/
/* greet() */
/* Expects a JwtAuthenticationToken to be retrieved from the security-context */
@@ -41,10 +62,12 @@ class MessageServiceUnitTest {
assertThrows(AccessDeniedException.class, () -> messageService.getSecret());
}
@Test
@WithMockJwtAuth(authorities = { "admin", "ROLE_AUTHORIZED_PERSONNEL" }, claims = @OpenIdClaims(preferredUsername = "ch4mpy"))
void givenSecurityContextIsPopulatedWithJwtAuthenticationToken_whenGreet_thenReturnGreetingWithPreferredUsernameAndAuthorities() {
assertEquals("Hello ch4mpy! You are granted with [admin, ROLE_AUTHORIZED_PERSONNEL].", messageService.greet());
@ParameterizedTest
@MethodSource("allIdentities")
void givenUserIsAuthenticated_whenGreet_thenReturnGreetingWithPreferredUsernameAndAuthorities(@ParameterizedAuthentication Authentication auth) {
final var jwt = (JwtAuthenticationToken) auth;
final var expected = "Hello %s! You are granted with %s.".formatted(jwt.getTokenAttributes().get(StandardClaimNames.PREFERRED_USERNAME), auth.getAuthorities());
assertEquals(expected, messageService.greet());
}
@Test
@@ -65,15 +88,22 @@ class MessageServiceUnitTest {
}
@Test
@WithMockJwtAuth(authorities = { "admin", "ROLE_AUTHORIZED_PERSONNEL" }, claims = @OpenIdClaims(preferredUsername = "ch4mpy"))
void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecret_thenReturnSecret() {
@WithJwt("ch4mpy.json")
void givenUserIsCh4mpy_whenGetSecret_thenReturnSecret() {
assertEquals("Only authorized personnel can read that", messageService.getSecret());
}
@Test
@WithMockJwtAuth(authorities = { "admin" }, claims = @OpenIdClaims(preferredUsername = "ch4mpy"))
void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecret_thenThrowsAccessDeniedException() {
@WithJwt("tonton-pirate.json")
void givenUserIsTontonPirate_whenGetSecret_thenThrowsAccessDeniedException() {
assertThrows(AccessDeniedException.class, () -> messageService.getSecret());
}
/*--------------------------------------------*/
/* methodSource returning all test identities */
/*--------------------------------------------*/
private Stream<AbstractAuthenticationToken> allIdentities() {
final var authentications = authFactory.authenticationsFrom("ch4mpy.json", "tonton-pirate.json").toList();
return authentications.stream();
}
}
@@ -12,8 +12,8 @@ import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.security.test.context.support.WithAnonymousUser;
import org.springframework.test.web.servlet.MockMvc;
import com.c4_soft.springaddons.security.oauth2.test.annotations.OpenIdClaims;
import com.c4_soft.springaddons.security.oauth2.test.annotations.WithMockJwtAuth;
import com.c4_soft.springaddons.security.oauth2.test.annotations.WithJwt;
import com.c4_soft.springaddons.security.oauth2.test.annotations.WithMockAuthentication;
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
@AutoConfigureMockMvc
@@ -34,7 +34,7 @@ class ServletResourceServerApplicationIntegrationTest {
}
@Test
@WithMockJwtAuth(authorities = { "admin", "ROLE_AUTHORIZED_PERSONNEL" }, claims = @OpenIdClaims(preferredUsername = "ch4mpy"))
@WithJwt("ch4mpy.json")
void givenUserIsAuthenticated_whenGetGreet_thenOk() throws Exception {
api.perform(get("/greet"))
.andExpect(status().isOk())
@@ -54,7 +54,7 @@ class ServletResourceServerApplicationIntegrationTest {
}
@Test
@WithMockJwtAuth("ROLE_AUTHORIZED_PERSONNEL")
@WithMockAuthentication("ROLE_AUTHORIZED_PERSONNEL")
void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenOk() throws Exception {
api.perform(get("/secured-route"))
.andExpect(status().isOk())
@@ -62,7 +62,7 @@ class ServletResourceServerApplicationIntegrationTest {
}
@Test
@WithMockJwtAuth("admin")
@WithMockAuthentication("admin")
void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenForbidden() throws Exception {
api.perform(get("/secured-route"))
.andExpect(status().isForbidden());
@@ -81,7 +81,7 @@ class ServletResourceServerApplicationIntegrationTest {
}
@Test
@WithMockJwtAuth("ROLE_AUTHORIZED_PERSONNEL")
@WithMockAuthentication("ROLE_AUTHORIZED_PERSONNEL")
void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenOk() throws Exception {
api.perform(get("/secured-method"))
.andExpect(status().isOk())
@@ -89,7 +89,7 @@ class ServletResourceServerApplicationIntegrationTest {
}
@Test
@WithMockJwtAuth("admin")
@WithMockAuthentication("admin")
void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenForbidden() throws Exception {
api.perform(get("/secured-method"))
.andExpect(status().isForbidden());
@@ -8,16 +8,19 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.security.core.Authentication;
import org.springframework.security.test.context.support.WithAnonymousUser;
import org.springframework.test.web.servlet.MockMvc;
import com.baeldung.ServletResourceServerApplication.GreetingController;
import com.baeldung.ServletResourceServerApplication.MessageService;
import com.c4_soft.springaddons.security.oauth2.test.annotations.OpenIdClaims;
import com.c4_soft.springaddons.security.oauth2.test.annotations.WithMockJwtAuth;
import com.c4_soft.springaddons.security.oauth2.test.annotations.WithMockAuthentication;
import com.c4_soft.springaddons.security.oauth2.test.annotations.parameterized.AuthenticationSource;
import com.c4_soft.springaddons.security.oauth2.test.annotations.parameterized.ParameterizedAuthentication;
@WebMvcTest(controllers = GreetingController.class, properties = { "server.ssl.enabled=false" })
class SpringAddonsGreetingControllerUnitTest {
@@ -40,9 +43,11 @@ class SpringAddonsGreetingControllerUnitTest {
.andExpect(status().isUnauthorized());
}
@Test
@WithMockJwtAuth(authorities = { "admin", "ROLE_AUTHORIZED_PERSONNEL" }, claims = @OpenIdClaims(preferredUsername = "ch4mpy"))
void givenUserIsAuthenticated_whenGetGreet_thenOk() throws Exception {
@ParameterizedTest
@AuthenticationSource({
@WithMockAuthentication(authorities = { "admin", "ROLE_AUTHORIZED_PERSONNEL" }, name = "ch4mpy"),
@WithMockAuthentication(authorities = { "uncle", "PIRATE" }, name = "tonton-pirate") })
void givenUserIsAuthenticated_whenGetGreet_thenOk(@ParameterizedAuthentication Authentication auth) throws Exception {
final var greeting = "Whatever the service returns";
when(messageService.greet()).thenReturn(greeting);
@@ -66,7 +71,7 @@ class SpringAddonsGreetingControllerUnitTest {
}
@Test
@WithMockJwtAuth({ "admin", "ROLE_AUTHORIZED_PERSONNEL" })
@WithMockAuthentication({ "admin", "ROLE_AUTHORIZED_PERSONNEL" })
void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenOk() throws Exception {
final var secret = "Secret!";
when(messageService.getSecret()).thenReturn(secret);
@@ -77,7 +82,7 @@ class SpringAddonsGreetingControllerUnitTest {
}
@Test
@WithMockJwtAuth({ "admin" })
@WithMockAuthentication({ "admin" })
void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenForbidden() throws Exception {
api.perform(get("/secured-route"))
.andExpect(status().isForbidden());
@@ -96,7 +101,7 @@ class SpringAddonsGreetingControllerUnitTest {
}
@Test
@WithMockJwtAuth({ "admin", "ROLE_AUTHORIZED_PERSONNEL" })
@WithMockAuthentication({ "admin", "ROLE_AUTHORIZED_PERSONNEL" })
void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenOk() throws Exception {
final var secret = "Secret!";
when(messageService.getSecret()).thenReturn(secret);
@@ -107,7 +112,7 @@ class SpringAddonsGreetingControllerUnitTest {
}
@Test
@WithMockJwtAuth(authorities = { "admin" })
@WithMockAuthentication({ "admin" })
void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenForbidden() throws Exception {
api.perform(get("/secured-method"))
.andExpect(status().isForbidden());
@@ -0,0 +1,15 @@
{
"iss": "https://localhost:8443/realms/master",
"sub": "281c4558-550c-413b-9972-2d2e5bde6b9b",
"iat": 1695992542,
"exp": 1695992642,
"preferred_username": "ch4mpy",
"realm_access": {
"roles": [
"admin",
"ROLE_AUTHORIZED_PERSONNEL"
]
},
"email": "ch4mp@c4-soft.com",
"scope": "openid email"
}
@@ -0,0 +1,15 @@
{
"iss": "https://localhost:8443/realms/master",
"sub": "2d2e5bde6b9b-550c-413b-9972-281c4558",
"iat": 1695992551,
"exp": 1695992651,
"preferred_username": "tonton-pirate",
"realm_access": {
"roles": [
"uncle",
"PIRATE"
]
},
"email": "tonton-pirate@c4-soft.com",
"scope": "openid email"
}