From 8f3b5ecc2b1ab7604607eb34e9de0eca068636f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Wacongne?= Date: Fri, 20 Oct 2023 05:41:41 +0200 Subject: [PATCH] spring-boot 3.1 and spring-addons 7.1.10 (#14902) --- .../spring-security-oauth2-testing/pom.xml | 2 +- .../ReactiveResourceServerApplication.java | 92 +++++++++-------- .../com/baeldung/MessageServiceUnitTest.java | 57 ++++++++--- ...ourceServerApplicationIntegrationTest.java | 14 +-- ...pringAddonsGreetingControllerUnitTest.java | 98 +++++++------------ .../src/test/resources/ch4mpy.json | 15 +++ .../src/test/resources/tonton-pirate.json | 15 +++ .../ServletResourceServerApplication.java | 62 ++++++------ .../com/baeldung/MessageServiceUnitTest.java | 56 ++++++++--- ...ourceServerApplicationIntegrationTest.java | 14 +-- ...pringAddonsGreetingControllerUnitTest.java | 23 +++-- .../src/test/resources/ch4mpy.json | 15 +++ .../src/test/resources/tonton-pirate.json | 15 +++ 13 files changed, 289 insertions(+), 189 deletions(-) create mode 100644 spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/test/resources/ch4mpy.json create mode 100644 spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/test/resources/tonton-pirate.json create mode 100644 spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/test/resources/ch4mpy.json create mode 100644 spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/test/resources/tonton-pirate.json diff --git a/spring-security-modules/spring-security-oauth2-testing/pom.xml b/spring-security-modules/spring-security-oauth2-testing/pom.xml index 93348cb48c..45fcf9bcce 100644 --- a/spring-security-modules/spring-security-oauth2-testing/pom.xml +++ b/spring-security-modules/spring-security-oauth2-testing/pom.xml @@ -14,7 +14,7 @@ ../../parent-boot-3 - 6.1.0 + 7.1.10 diff --git a/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/main/java/com/baeldung/ReactiveResourceServerApplication.java b/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/main/java/com/baeldung/ReactiveResourceServerApplication.java index 500d876bc4..716900ea51 100644 --- a/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/main/java/com/baeldung/ReactiveResourceServerApplication.java +++ b/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/main/java/com/baeldung/ReactiveResourceServerApplication.java @@ -1,7 +1,8 @@ package com.baeldung; +import static org.springframework.security.config.Customizer.withDefaults; + import java.nio.charset.Charset; -import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Optional; @@ -27,6 +28,7 @@ import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.oauth2.core.oidc.StandardClaimNames; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverter; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository; import org.springframework.stereotype.Service; @@ -34,6 +36,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import lombok.RequiredArgsConstructor; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @SpringBootApplication @@ -46,68 +49,66 @@ public class ReactiveResourceServerApplication { @Configuration @EnableWebFluxSecurity @EnableReactiveMethodSecurity - public class SecurityConfig { + static class SecurityConfig { @Bean - SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, Converter>> authoritiesConverter) { - http.oauth2ResourceServer() - .jwt() - .jwtAuthenticationConverter(jwt -> authoritiesConverter.convert(jwt) - .map(authorities -> new JwtAuthenticationToken(jwt, authorities))); - http.securityContextRepository(NoOpServerSecurityContextRepository.getInstance()) - .csrf() - .disable(); - http.exceptionHandling() - .accessDeniedHandler((var exchange, var ex) -> exchange.getPrincipal() - .flatMap(principal -> { + SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http.oauth2ResourceServer(resourceServer -> resourceServer.jwt(withDefaults())); + http.securityContextRepository(NoOpServerSecurityContextRepository.getInstance()); + http.csrf(csrf -> csrf.disable()); + http.exceptionHandling(eh -> eh + .accessDeniedHandler((var exchange, var ex) -> exchange.getPrincipal().flatMap(principal -> { final var response = exchange.getResponse(); - response.setStatusCode(principal instanceof AnonymousAuthenticationToken ? HttpStatus.UNAUTHORIZED : HttpStatus.FORBIDDEN); - response.getHeaders() - .setContentType(MediaType.TEXT_PLAIN); + response.setStatusCode( + principal instanceof AnonymousAuthenticationToken ? HttpStatus.UNAUTHORIZED + : HttpStatus.FORBIDDEN); + response.getHeaders().setContentType(MediaType.TEXT_PLAIN); final var dataBufferFactory = response.bufferFactory(); - final var buffer = dataBufferFactory.wrap(ex.getMessage() - .getBytes(Charset.defaultCharset())); + final var buffer = dataBufferFactory.wrap(ex.getMessage().getBytes(Charset.defaultCharset())); return response.writeWith(Mono.just(buffer)) - .doOnError(error -> DataBufferUtils.release(buffer)); - })); + .doOnError(error -> DataBufferUtils.release(buffer)); + }))); - http.authorizeExchange() - .pathMatchers("/secured-route") - .hasRole("AUTHORIZED_PERSONNEL") - .anyExchange() - .authenticated(); + // @formatter:off + http.authorizeExchange(req -> req + .pathMatchers("/secured-route").hasRole("AUTHORIZED_PERSONNEL").anyExchange() + .authenticated()); + // @formatter:on return http.build(); } - static interface AuthoritiesConverter extends Converter>> { + static interface ReactiveJwtAuthoritiesConverter extends Converter> { } @Bean - AuthoritiesConverter realmRoles2AuthoritiesConverter() { + ReactiveJwtAuthoritiesConverter 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) realmRoles.getOrDefault("roles", List.of()); - return Mono.just(roles.stream() - .map(SimpleGrantedAuthority::new) - .map(GrantedAuthority.class::cast) - .toList()); + return Flux.fromStream(roles.stream()).map(SimpleGrantedAuthority::new) + .map(GrantedAuthority.class::cast); }; } + + @Bean + ReactiveJwtAuthenticationConverter authenticationConverter( + Converter> authoritiesConverter) { + final var authenticationConverter = new ReactiveJwtAuthenticationConverter(); + authenticationConverter.setPrincipalClaimName(StandardClaimNames.PREFERRED_USERNAME); + authenticationConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter); + return authenticationConverter; + } } @Service public static class MessageService { public Mono greet() { - return ReactiveSecurityContextHolder.getContext() - .map(ctx -> { - final var who = (JwtAuthenticationToken) ctx.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()); - }) - .switchIfEmpty(Mono.error(new AuthenticationCredentialsNotFoundException("Security context is empty"))); + return ReactiveSecurityContextHolder.getContext().map(ctx -> { + final var who = (JwtAuthenticationToken) ctx.getAuthentication(); + return "Hello %s! You are granted with %s.".formatted(who.getName(), who.getAuthorities()); + }).switchIfEmpty(Mono.error(new AuthenticationCredentialsNotFoundException("Security context is empty"))); } @PreAuthorize("hasRole('AUTHORIZED_PERSONNEL')") @@ -118,26 +119,23 @@ public class ReactiveResourceServerApplication { @RestController @RequiredArgsConstructor - public class GreetingController { + public static class GreetingController { private final MessageService messageService; @GetMapping("/greet") public Mono> greet() { - return messageService.greet() - .map(ResponseEntity::ok); + return messageService.greet().map(ResponseEntity::ok); } @GetMapping("/secured-route") public Mono> securedRoute() { - return messageService.getSecret() - .map(ResponseEntity::ok); + return messageService.getSecret().map(ResponseEntity::ok); } @GetMapping("/secured-method") @PreAuthorize("hasRole('AUTHORIZED_PERSONNEL')") public Mono> securedMethod() { - return messageService.getSecret() - .map(ResponseEntity::ok); + return messageService.getSecret().map(ResponseEntity::ok); } } diff --git a/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/test/java/com/baeldung/MessageServiceUnitTest.java b/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/test/java/com/baeldung/MessageServiceUnitTest.java index 97893bc1fb..c13a20ca38 100644 --- a/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/test/java/com/baeldung/MessageServiceUnitTest.java +++ b/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/test/java/com/baeldung/MessageServiceUnitTest.java @@ -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.EnableReactiveMethodSecurity; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.oidc.StandardClaimNames; +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; +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.ReactiveResourceServerApplication.MessageService; -import com.c4_soft.springaddons.security.oauth2.test.annotations.OpenIdClaims; -import com.c4_soft.springaddons.security.oauth2.test.annotations.WithMockJwtAuth; +import com.baeldung.ReactiveResourceServerApplication.SecurityConfig; +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, SecurityConfig.class }) +@ImportAutoConfiguration(AuthenticationFactoriesTestConf.class) @ExtendWith(SpringExtension.class) -@EnableReactiveMethodSecurity +@TestInstance(Lifecycle.PER_CLASS) class MessageServiceUnitTest { @Autowired MessageService messageService; + @Autowired + WithJwt.AuthenticationFactory authFactory; + + @MockBean + ReactiveJwtDecoder jwtDecoder; + /*----------------------------------------------------------------------------*/ /* greet() */ /* Expects a JwtAuthenticationToken to be retrieved from the security-context */ @@ -43,10 +64,12 @@ class MessageServiceUnitTest { .block()); } - @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() .block()); } @@ -70,17 +93,25 @@ 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() .block()); } @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() .block()); } + /*--------------------------------------------*/ + /* methodSource returning all test identities */ + /*--------------------------------------------*/ + private Stream allIdentities() { + final var authentications = authFactory.authenticationsFrom("ch4mpy.json", "tonton-pirate.json").toList(); + return authentications.stream(); + } + } diff --git a/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/test/java/com/baeldung/ReactiveResourceServerApplicationIntegrationTest.java b/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/test/java/com/baeldung/ReactiveResourceServerApplicationIntegrationTest.java index 1ee6fc7e87..d6bfbf4e2d 100644 --- a/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/test/java/com/baeldung/ReactiveResourceServerApplicationIntegrationTest.java +++ b/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/test/java/com/baeldung/ReactiveResourceServerApplicationIntegrationTest.java @@ -8,8 +8,8 @@ import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.security.test.context.support.WithAnonymousUser; import org.springframework.test.web.reactive.server.WebTestClient; -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) @AutoConfigureWebTestClient @@ -33,7 +33,7 @@ class ReactiveResourceServerApplicationIntegrationTest { } @Test - @WithMockJwtAuth(authorities = { "admin", "ROLE_AUTHORIZED_PERSONNEL" }, claims = @OpenIdClaims(preferredUsername = "ch4mpy")) + @WithJwt("ch4mpy.json") void givenUserIsAuthenticated_whenGetGreet_thenOk() throws Exception { api.get() .uri("/greet") @@ -60,7 +60,7 @@ class ReactiveResourceServerApplicationIntegrationTest { } @Test - @WithMockJwtAuth("ROLE_AUTHORIZED_PERSONNEL") + @WithMockAuthentication("ROLE_AUTHORIZED_PERSONNEL") void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenOk() throws Exception { api.get() .uri("/secured-route") @@ -72,7 +72,7 @@ class ReactiveResourceServerApplicationIntegrationTest { } @Test - @WithMockJwtAuth("admin") + @WithMockAuthentication("admin") void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenForbidden() throws Exception { api.get() .uri("/secured-route") @@ -97,7 +97,7 @@ class ReactiveResourceServerApplicationIntegrationTest { } @Test - @WithMockJwtAuth("ROLE_AUTHORIZED_PERSONNEL") + @WithMockAuthentication("ROLE_AUTHORIZED_PERSONNEL") void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenOk() throws Exception { api.get() .uri("/secured-method") @@ -109,7 +109,7 @@ class ReactiveResourceServerApplicationIntegrationTest { } @Test - @WithMockJwtAuth("admin") + @WithMockAuthentication("admin") void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenForbidden() throws Exception { api.get() .uri("/secured-method") diff --git a/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/test/java/com/baeldung/SpringAddonsGreetingControllerUnitTest.java b/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/test/java/com/baeldung/SpringAddonsGreetingControllerUnitTest.java index 6f55f287d8..f31bbe3ae8 100644 --- a/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/test/java/com/baeldung/SpringAddonsGreetingControllerUnitTest.java +++ b/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/test/java/com/baeldung/SpringAddonsGreetingControllerUnitTest.java @@ -5,16 +5,19 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; 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.reactive.WebFluxTest; 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.reactive.server.WebTestClient; import com.baeldung.ReactiveResourceServerApplication.GreetingController; import com.baeldung.ReactiveResourceServerApplication.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; import reactor.core.publisher.Mono; @@ -28,115 +31,88 @@ class SpringAddonsGreetingControllerUnitTest { WebTestClient api; /*-----------------------------------------------------------------------------*/ - /* /greet */ - /* This end-point secured with ".anyRequest().authenticated()" in SecurityConf */ + /* /greet */ + /* + * This end-point secured with ".anyRequest().authenticated()" in SecurityConf + */ /*-----------------------------------------------------------------------------*/ @Test @WithAnonymousUser void givenRequestIsAnonymous_whenGetGreet_thenUnauthorized() throws Exception { - api.get() - .uri("/greet") - .exchange() - .expectStatus() - .isUnauthorized(); + api.get().uri("/greet").exchange().expectStatus().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(Mono.just(greeting)); - api.get() - .uri("/greet") - .exchange() - .expectStatus() - .isOk() - .expectBody(String.class) - .isEqualTo(greeting); + api.get().uri("/greet").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo(greeting); verify(messageService, times(1)).greet(); } /*---------------------------------------------------------------------------------------------------------------------*/ - /* /secured-route */ - /* This end-point is secured with ".requestMatchers("/secured-route").hasRole("AUTHORIZED_PERSONNEL")" in SecurityConf */ + /* /secured-route */ + /* + * This end-point is secured with + * ".requestMatchers("/secured-route").hasRole("AUTHORIZED_PERSONNEL")" in + * SecurityConf + */ /*---------------------------------------------------------------------------------------------------------------------*/ @Test @WithAnonymousUser void givenRequestIsAnonymous_whenGetSecuredRoute_thenUnauthorized() throws Exception { - api.get() - .uri("/secured-route") - .exchange() - .expectStatus() - .isUnauthorized(); + api.get().uri("/secured-route").exchange().expectStatus().isUnauthorized(); } @Test - @WithMockJwtAuth("ROLE_AUTHORIZED_PERSONNEL") + @WithMockAuthentication("ROLE_AUTHORIZED_PERSONNEL") void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenOk() throws Exception { final var secret = "Secret!"; when(messageService.getSecret()).thenReturn(Mono.just(secret)); - api.get() - .uri("/secured-route") - .exchange() - .expectStatus() - .isOk() - .expectBody(String.class) - .isEqualTo(secret); + api.get().uri("/secured-route").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo(secret); } @Test - @WithMockJwtAuth("admin") + @WithMockAuthentication("admin") void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenForbidden() throws Exception { - api.get() - .uri("/secured-route") - .exchange() - .expectStatus() - .isForbidden(); + api.get().uri("/secured-route").exchange().expectStatus().isForbidden(); } /*---------------------------------------------------------------------------------------------------------*/ - /* /secured-method */ - /* This end-point is secured with "@PreAuthorize("hasRole('AUTHORIZED_PERSONNEL')")" on @Controller method */ + /* /secured-method */ + /* + * This end-point is secured with + * "@PreAuthorize("hasRole('AUTHORIZED_PERSONNEL')")" on @Controller method + */ /*---------------------------------------------------------------------------------------------------------*/ @Test @WithAnonymousUser void givenRequestIsAnonymous_whenGetSecuredMethod_thenUnauthorized() throws Exception { - api.get() - .uri("/secured-method") - .exchange() - .expectStatus() - .isUnauthorized(); + api.get().uri("/secured-method").exchange().expectStatus().isUnauthorized(); } @Test - @WithMockJwtAuth("ROLE_AUTHORIZED_PERSONNEL") + @WithMockAuthentication("ROLE_AUTHORIZED_PERSONNEL") void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenOk() throws Exception { final var secret = "Secret!"; when(messageService.getSecret()).thenReturn(Mono.just(secret)); - api.get() - .uri("/secured-method") - .exchange() - .expectStatus() - .isOk() - .expectBody(String.class) - .isEqualTo(secret); + api.get().uri("/secured-method").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo(secret); } @Test - @WithMockJwtAuth("admin") + @WithMockAuthentication("admin") void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenForbidden() throws Exception { - api.get() - .uri("/secured-method") - .exchange() - .expectStatus() - .isForbidden(); + api.get().uri("/secured-method").exchange().expectStatus().isForbidden(); } } diff --git a/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/test/resources/ch4mpy.json b/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/test/resources/ch4mpy.json new file mode 100644 index 0000000000..22f7bb2cea --- /dev/null +++ b/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/test/resources/ch4mpy.json @@ -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" +} \ No newline at end of file diff --git a/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/test/resources/tonton-pirate.json b/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/test/resources/tonton-pirate.json new file mode 100644 index 0000000000..13a422f6fd --- /dev/null +++ b/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/test/resources/tonton-pirate.json @@ -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" +} \ No newline at end of file diff --git a/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/main/java/com/baeldung/ServletResourceServerApplication.java b/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/main/java/com/baeldung/ServletResourceServerApplication.java index a30c60eab0..8258955afe 100644 --- a/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/main/java/com/baeldung/ServletResourceServerApplication.java +++ b/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/main/java/com/baeldung/ServletResourceServerApplication.java @@ -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> 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> { + static interface JwtAuthoritiesConverter extends Converter> { } @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) 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> 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')") diff --git a/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/test/java/com/baeldung/MessageServiceUnitTest.java b/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/test/java/com/baeldung/MessageServiceUnitTest.java index 3c608d226e..ca237fb888 100644 --- a/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/test/java/com/baeldung/MessageServiceUnitTest.java +++ b/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/test/java/com/baeldung/MessageServiceUnitTest.java @@ -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 allIdentities() { + final var authentications = authFactory.authenticationsFrom("ch4mpy.json", "tonton-pirate.json").toList(); + return authentications.stream(); + } } diff --git a/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/test/java/com/baeldung/ServletResourceServerApplicationIntegrationTest.java b/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/test/java/com/baeldung/ServletResourceServerApplicationIntegrationTest.java index 5bb539741f..4f2fe51787 100644 --- a/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/test/java/com/baeldung/ServletResourceServerApplicationIntegrationTest.java +++ b/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/test/java/com/baeldung/ServletResourceServerApplicationIntegrationTest.java @@ -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()); diff --git a/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/test/java/com/baeldung/SpringAddonsGreetingControllerUnitTest.java b/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/test/java/com/baeldung/SpringAddonsGreetingControllerUnitTest.java index 9162768930..2534d9919a 100644 --- a/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/test/java/com/baeldung/SpringAddonsGreetingControllerUnitTest.java +++ b/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/test/java/com/baeldung/SpringAddonsGreetingControllerUnitTest.java @@ -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()); diff --git a/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/test/resources/ch4mpy.json b/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/test/resources/ch4mpy.json new file mode 100644 index 0000000000..22f7bb2cea --- /dev/null +++ b/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/test/resources/ch4mpy.json @@ -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" +} \ No newline at end of file diff --git a/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/test/resources/tonton-pirate.json b/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/test/resources/tonton-pirate.json new file mode 100644 index 0000000000..13a422f6fd --- /dev/null +++ b/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/test/resources/tonton-pirate.json @@ -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" +} \ No newline at end of file