diff --git a/pom.xml b/pom.xml index 0ec9e9efdd..345cd7979a 100644 --- a/pom.xml +++ b/pom.xml @@ -1023,6 +1023,7 @@ spring-kafka spring-native + spring-security-modules/spring-security-oauth2-testing spring-protobuf spring-quartz diff --git a/spring-security-modules/spring-security-oauth2-testing/README.md b/spring-security-modules/spring-security-oauth2-testing/README.md new file mode 100644 index 0000000000..82728ff450 --- /dev/null +++ b/spring-security-modules/spring-security-oauth2-testing/README.md @@ -0,0 +1,7 @@ +## Spring-Security 6 OAuth2 testing samples + +This module contains articles about testing spring-security 6 access-control with OAuth2 + +## Relevant articles: + +- [Testing Spring OAuth2 Access-Control](https://drafts.baeldung.com/?p=154767&preview=true) diff --git a/spring-security-modules/spring-security-oauth2-testing/pom.xml b/spring-security-modules/spring-security-oauth2-testing/pom.xml new file mode 100644 index 0000000000..f634b6105c --- /dev/null +++ b/spring-security-modules/spring-security-oauth2-testing/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + spring-security-oauth2-testing + spring-security-oauth2-testing + pom + spring-security 6 oauth testing sample project + + com.baeldung + parent-boot-3 + 0.0.1-SNAPSHOT + ../../parent-boot-3 + + + 6.0.14 + + + + + com.c4-soft.springaddons + spring-addons-oauth2-test + ${spring-addons.version} + + + + + reactive-resource-server + servlet-resource-server + + \ No newline at end of file diff --git a/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/pom.xml b/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/pom.xml new file mode 100644 index 0000000000..c1fa6b7c6d --- /dev/null +++ b/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + com.baeldung + spring-security-oauth2-testing + 0.0.1-SNAPSHOT + + com.baeldung.spring-security-modules.testing + reactive-resource-server + reactive-resource-server + Demo project for Spring Boot reactive resource-server + + 17 + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + org.springframework.boot + spring-boot-starter-webflux + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + com.c4-soft.springaddons + spring-addons-oauth2-test + test + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + \ No newline at end of file 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 new file mode 100644 index 0000000000..608038331a --- /dev/null +++ b/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/main/java/com/baeldung/ReactiveResourceServerApplication.java @@ -0,0 +1,124 @@ +package com.baeldung; + +import java.nio.charset.Charset; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +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.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository; +import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; +import reactor.core.publisher.Mono; + +@SpringBootApplication +public class ReactiveResourceServerApplication { + + public static void main(String[] args) { + SpringApplication.run(ReactiveResourceServerApplication.class, args); + } + + @Configuration + @EnableWebFluxSecurity + @EnableReactiveMethodSecurity + public 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 -> { + final var response = exchange.getResponse(); + 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())); + return response.writeWith(Mono.just(buffer)).doOnError(error -> DataBufferUtils.release(buffer)); + })); + + http.authorizeExchange().pathMatchers("/secured-route").hasRole("AUTHORIZED_PERSONNEL").anyExchange().authenticated(); + + return http.build(); + } + + static interface AuthoritiesConverter extends Converter>> { + } + + @Bean + AuthoritiesConverter realmRoles2AuthoritiesConverter() { + return (Jwt jwt) -> { + 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()); + }; + } + } + + @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"))); + } + + @PreAuthorize("hasRole('AUTHORIZED_PERSONNEL')") + public Mono getSecret() { + return Mono.just("Only authorized personnel can read that"); + } + } + + @RestController + @RequiredArgsConstructor + public class GreetingController { + private final MessageService messageService; + + @GetMapping("/greet") + public Mono> greet() { + return messageService.greet().map(ResponseEntity::ok); + } + + @GetMapping("/secured-route") + public Mono> securedRoute() { + return messageService.getSecret().map(ResponseEntity::ok); + } + + @GetMapping("/secured-method") + @PreAuthorize("hasRole('AUTHORIZED_PERSONNEL')") + public Mono> securedMethod() { + return messageService.getSecret().map(ResponseEntity::ok); + } + } + +} diff --git a/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/main/resources/application.yaml b/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/main/resources/application.yaml new file mode 100644 index 0000000000..01e655e1b3 --- /dev/null +++ b/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/main/resources/application.yaml @@ -0,0 +1,6 @@ +spring: + security: + oauth2: + resourceserver: + jwt: + issuer-uri: https://localhost:8443/realms/master 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 new file mode 100644 index 0000000000..92896754ef --- /dev/null +++ b/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/test/java/com/baeldung/MessageServiceUnitTest.java @@ -0,0 +1,79 @@ +package com.baeldung; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity; +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({ MessageService.class }) +@ExtendWith(SpringExtension.class) +@EnableReactiveMethodSecurity +class MessageServiceUnitTest { + @Autowired + MessageService messageService; + + /*----------------------------------------------------------------------------*/ + /* greet() */ + /* Expects a JwtAuthenticationToken to be retrieved from the security-context */ + /*----------------------------------------------------------------------------*/ + + @Test + void givenSecurityContextIsEmpty_whenGreet_thenThrowsAuthenticationCredentialsNotFoundException() { + assertThrows(AuthenticationCredentialsNotFoundException.class, () -> messageService.greet().block()); + } + + @Test + @WithAnonymousUser + void givenUserIsNotAuthenticated_whenGreet_thenThrowsClassCastException() { + assertThrows(ClassCastException.class, () -> messageService.greet().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().block()); + } + + @Test + @WithMockUser(authorities = { "admin", "ROLE_AUTHORIZED_PERSONNEL" }, username = "ch4mpy") + void givenSecurityContextIsPopulatedWithUsernamePasswordAuthenticationToken_whenGreet_thenThrowsClassCastException() { + assertThrows(ClassCastException.class, () -> messageService.greet().block()); + } + + /*--------------------------------------------------------------------*/ + /* getSecret() */ + /* is secured with "@PreAuthorize("hasRole('AUTHORIZED_PERSONNEL')")" */ + /*--------------------------------------------------------------------*/ + + @Test + @WithAnonymousUser + void givenUserIsNotAuthenticated_whenGetSecret_thenThrowsAccessDeniedException() { + assertThrows(AccessDeniedException.class, () -> messageService.getSecret().block()); + } + + @Test + @WithMockJwtAuth(authorities = { "admin", "ROLE_AUTHORIZED_PERSONNEL" }, claims = @OpenIdClaims(preferredUsername = "ch4mpy")) + void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecret_thenReturnSecret() { + assertEquals("Only authorized personnel can read that", messageService.getSecret().block()); + } + + @Test + @WithMockJwtAuth(authorities = { "admin" }, claims = @OpenIdClaims(preferredUsername = "ch4mpy")) + void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecret_thenThrowsAccessDeniedException() { + assertThrows(AccessDeniedException.class, () -> messageService.getSecret().block()); + } + +} 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 new file mode 100644 index 0000000000..e3c45883c2 --- /dev/null +++ b/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/test/java/com/baeldung/ReactiveResourceServerApplicationIntegrationTest.java @@ -0,0 +1,109 @@ +package com.baeldung; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; +import org.springframework.boot.test.context.SpringBootTest; +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; + +@SpringBootTest(webEnvironment = WebEnvironment.MOCK) +@AutoConfigureWebTestClient +class ReactiveResourceServerApplicationIntegrationTest { + @Autowired + WebTestClient api; + + /*-----------------------------------------------------------------------------*/ + /* /greet */ + /* This end-point secured with ".anyRequest().authenticated()" in SecurityConf */ + /*-----------------------------------------------------------------------------*/ + + @Test + @WithAnonymousUser + void givenUserIsNotAuthenticated_whenGetGreet_thenUnauthorized() throws Exception { + // @formatter:off + api.get().uri("/greet").exchange() + .expectStatus().isUnauthorized(); + // @formatter:on + } + + @Test + @WithMockJwtAuth(authorities = { "admin", "ROLE_AUTHORIZED_PERSONNEL" }, claims = @OpenIdClaims(preferredUsername = "ch4mpy")) + void givenUserIsAuthenticated_whenGetGreet_thenOk() throws Exception { + // @formatter:off + api.get().uri("/greet").exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo("Hello ch4mpy! You are granted with [admin, ROLE_AUTHORIZED_PERSONNEL]."); + // @formatter:on + } + + /*---------------------------------------------------------------------------------------------------------------------*/ + /* /secured-route */ + /* This end-point is secured with ".requestMatchers("/secured-route").hasRole("AUTHORIZED_PERSONNEL")" in SecurityConf */ + /*---------------------------------------------------------------------------------------------------------------------*/ + + @Test + @WithAnonymousUser + void givenUserIsNotAuthenticated_whenGetSecuredRoute_thenUnauthorized() throws Exception { + // @formatter:off + api.get().uri("/secured-route").exchange() + .expectStatus().isUnauthorized(); + // @formatter:on + } + + @Test + @WithMockJwtAuth("ROLE_AUTHORIZED_PERSONNEL") + void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenOk() throws Exception { + // @formatter:off + api.get().uri("/secured-route").exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo("Only authorized personnel can read that"); + // @formatter:on + } + + @Test + @WithMockJwtAuth("admin") + void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenForbidden() throws Exception { + // @formatter:off + api.get().uri("/secured-route").exchange() + .expectStatus().isForbidden(); + // @formatter:on + } + + /*---------------------------------------------------------------------------------------------------------*/ + /* /secured-method */ + /* This end-point is secured with "@PreAuthorize("hasRole('AUTHORIZED_PERSONNEL')")" on @Controller method */ + /*---------------------------------------------------------------------------------------------------------*/ + + @Test + @WithAnonymousUser + void givenUserIsNotAuthenticated_whenGetSecuredMethod_thenUnauthorized() throws Exception { + // @formatter:off + api.get().uri("/secured-method").exchange() + .expectStatus().isUnauthorized(); + // @formatter:on + } + + @Test + @WithMockJwtAuth("ROLE_AUTHORIZED_PERSONNEL") + void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenOk() throws Exception { + // @formatter:off + api.get().uri("/secured-method").exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo("Only authorized personnel can read that"); + // @formatter:on + } + + @Test + @WithMockJwtAuth("admin") + void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenForbidden() throws Exception { + // @formatter:off + api.get().uri("/secured-method").exchange() + .expectStatus().isForbidden(); + // @formatter:on + } +} 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 new file mode 100644 index 0000000000..e28136dd25 --- /dev/null +++ b/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/test/java/com/baeldung/SpringAddonsGreetingControllerUnitTest.java @@ -0,0 +1,123 @@ +package com.baeldung; + +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.Test; +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.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 reactor.core.publisher.Mono; + +@WebFluxTest(controllers = GreetingController.class, properties = { "server.ssl.enabled=false" }) +class SpringAddonsGreetingControllerUnitTest { + + @MockBean + MessageService messageService; + + @Autowired + WebTestClient api; + + /*-----------------------------------------------------------------------------*/ + /* /greet */ + /* This end-point secured with ".anyRequest().authenticated()" in SecurityConf */ + /*-----------------------------------------------------------------------------*/ + + @Test + @WithAnonymousUser + void givenUserIsNotAuthenticated_whenGetGreet_thenUnauthorized() throws Exception { + api.get().uri("/greet").exchange().expectStatus().isUnauthorized(); + } + + @Test + @WithMockJwtAuth(claims = @OpenIdClaims(preferredUsername = "Tonton Pirate")) + void givenUserIsAuthenticated_whenGetGreet_thenOk() throws Exception { + final var greeting = "Whatever the service returns"; + when(messageService.greet()).thenReturn(Mono.just(greeting)); + + // @formatter:off + api.get().uri("/greet").exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo(greeting); + // @formatter:on + + verify(messageService, times(1)).greet(); + } + + /*---------------------------------------------------------------------------------------------------------------------*/ + /* /secured-route */ + /* This end-point is secured with ".requestMatchers("/secured-route").hasRole("AUTHORIZED_PERSONNEL")" in SecurityConf */ + /*---------------------------------------------------------------------------------------------------------------------*/ + + @Test + @WithAnonymousUser + void givenUserIsNotAuthenticated_whenGetSecuredRoute_thenUnauthorized() throws Exception { + api.get().uri("/secured-route").exchange().expectStatus().isUnauthorized(); + } + + @Test + @WithMockJwtAuth("ROLE_AUTHORIZED_PERSONNEL") + void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenOk() throws Exception { + final var secret = "Secret!"; + when(messageService.getSecret()).thenReturn(Mono.just(secret)); + + // @formatter:off + api.get().uri("/secured-route").exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo(secret); + // @formatter:on + } + + @Test + @WithMockJwtAuth("admin") + void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenForbidden() throws Exception { + // @formatter:off + api.get().uri("/secured-route").exchange() + .expectStatus().isForbidden(); + // @formatter:on + } + + /*---------------------------------------------------------------------------------------------------------*/ + /* /secured-method */ + /* This end-point is secured with "@PreAuthorize("hasRole('AUTHORIZED_PERSONNEL')")" on @Controller method */ + /*---------------------------------------------------------------------------------------------------------*/ + + @Test + @WithAnonymousUser + void givenUserIsNotAuthenticated_whenGetSecuredMethod_thenUnauthorized() throws Exception { + api.get().uri("/secured-method").exchange().expectStatus().isUnauthorized(); + } + + @Test + @WithMockJwtAuth("ROLE_AUTHORIZED_PERSONNEL") + void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenOk() throws Exception { + final var secret = "Secret!"; + when(messageService.getSecret()).thenReturn(Mono.just(secret)); + + // @formatter:off + api.get().uri("/secured-method").exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo(secret); + // @formatter:on + } + + @Test + @WithMockJwtAuth("admin") + void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenForbidden() throws Exception { + // @formatter:off + api.get().uri("/secured-method").exchange() + .expectStatus().isForbidden(); + // @formatter:on + } + +} + diff --git a/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/test/java/com/baeldung/SpringSecurityTestGreetingControllerUnitTest.java b/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/test/java/com/baeldung/SpringSecurityTestGreetingControllerUnitTest.java new file mode 100644 index 0000000000..291a405e29 --- /dev/null +++ b/spring-security-modules/spring-security-oauth2-testing/reactive-resource-server/src/test/java/com/baeldung/SpringSecurityTestGreetingControllerUnitTest.java @@ -0,0 +1,138 @@ +package com.baeldung; + +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockAuthentication; +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockJwt; + +import org.junit.jupiter.api.Test; +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.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.test.web.reactive.server.WebTestClient; + +import com.baeldung.ReactiveResourceServerApplication.GreetingController; +import com.baeldung.ReactiveResourceServerApplication.MessageService; + +import reactor.core.publisher.Mono; + +@WebFluxTest(controllers = GreetingController.class, properties = { "server.ssl.enabled=false" }) +class SpringSecurityTestGreetingControllerUnitTest { + private static final AnonymousAuthenticationToken ANONYMOUS_AUTHENTICATION = new AnonymousAuthenticationToken( + "anonymous", + "anonymousUser", + AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); + + @MockBean + MessageService messageService; + + @Autowired + WebTestClient api; + + /*-----------------------------------------------------------------------------*/ + /* /greet */ + /* This end-point secured with ".anyRequest().authenticated()" in SecurityConf */ + /*-----------------------------------------------------------------------------*/ + + @Test + void givenUserIsNotAuthenticated_whenGetGreet_thenUnauthorized() throws Exception { + // @formatter:off + api.mutateWith(mockAuthentication(ANONYMOUS_AUTHENTICATION)) + .get().uri("/greet").exchange() + .expectStatus().isUnauthorized(); + // @formatter:on + } + + @Test + void givenUserIsAuthenticated_whenGetGreet_thenOk() throws Exception { + final var greeting = "Whatever the service returns"; + when(messageService.greet()).thenReturn(Mono.just(greeting)); + + // @formatter:off + api.mutateWith(mockJwt()) + .get().uri("/greet").exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo(greeting); + // @formatter:on + + verify(messageService, times(1)).greet(); + } + + /*---------------------------------------------------------------------------------------------------------------------*/ + /* /secured-route */ + /* This end-point is secured with ".requestMatchers("/secured-route").hasRole("AUTHORIZED_PERSONNEL")" in SecurityConf */ + /*---------------------------------------------------------------------------------------------------------------------*/ + + @Test + void givenUserIsNotAuthenticated_whenGetSecuredRoute_thenUnauthorized() throws Exception { + // @formatter:off + api.mutateWith(mockAuthentication(ANONYMOUS_AUTHENTICATION)) + .get().uri("/secured-route").exchange() + .expectStatus().isUnauthorized(); + // @formatter:on + } + + @Test + void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenOk() throws Exception { + final var secret = "Secret!"; + when(messageService.getSecret()).thenReturn(Mono.just(secret)); + + // @formatter:off + api.mutateWith(mockJwt().authorities(new SimpleGrantedAuthority("ROLE_AUTHORIZED_PERSONNEL"))) + .get().uri("/secured-route").exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo(secret); + // @formatter:on + } + + @Test + void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenForbidden() throws Exception { + // @formatter:off + api.mutateWith(mockJwt().authorities(new SimpleGrantedAuthority("admin"))) + .get().uri("/secured-route").exchange() + .expectStatus().isForbidden(); + // @formatter:on + } + + /*---------------------------------------------------------------------------------------------------------*/ + /* /secured-method */ + /* This end-point is secured with "@PreAuthorize("hasRole('AUTHORIZED_PERSONNEL')")" on @Controller method */ + /*---------------------------------------------------------------------------------------------------------*/ + + @Test + void givenUserIsNotAuthenticated_whenGetSecuredMethod_thenUnauthorized() throws Exception { + // @formatter:off + api.mutateWith(mockAuthentication(ANONYMOUS_AUTHENTICATION)) + .get().uri("/secured-method").exchange() + .expectStatus().isUnauthorized(); + // @formatter:on + } + + @Test + void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenOk() throws Exception { + final var secret = "Secret!"; + when(messageService.getSecret()).thenReturn(Mono.just(secret)); + + // @formatter:off + api.mutateWith(mockJwt().authorities(new SimpleGrantedAuthority("ROLE_AUTHORIZED_PERSONNEL"))) + .get().uri("/secured-method").exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo(secret); + // @formatter:on + } + + @Test + void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenForbidden() throws Exception { + // @formatter:off + api.mutateWith(mockJwt().authorities(new SimpleGrantedAuthority("admin"))) + .get().uri("/secured-method").exchange() + .expectStatus().isForbidden(); + // @formatter:on + } + +} + diff --git a/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/pom.xml b/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/pom.xml new file mode 100644 index 0000000000..8de154b934 --- /dev/null +++ b/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + com.baeldung + spring-security-oauth2-testing + 0.0.1-SNAPSHOT + + com.baeldung.spring-security-modules.testing + servlet-resource-server + servlet-resource-server + Demo project for Spring Boot servlet resource-server + + 17 + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + org.springframework.boot + spring-boot-starter-web + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + com.c4-soft.springaddons + spring-addons-oauth2-test + test + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + 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 new file mode 100644 index 0000000000..ba55944c29 --- /dev/null +++ b/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/main/java/com/baeldung/ServletResourceServerApplication.java @@ -0,0 +1,116 @@ +package com.baeldung; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.GrantedAuthority; +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.JwtAuthenticationToken; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; + +@SpringBootApplication +public class ServletResourceServerApplication { + + public static void main(String[] args) { + SpringApplication.run(ServletResourceServerApplication.class, args); + } + + @Configuration + @EnableMethodSecurity + @EnableWebSecurity + static class SecurityConf { + @Bean + SecurityFilterChain filterChain(HttpSecurity http, Converter> authoritiesConverter) throws Exception { + // @formatter:off + 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()); + }); + + http.authorizeHttpRequests() + .requestMatchers("/secured-route").hasRole("AUTHORIZED_PERSONNEL") + .anyRequest().authenticated(); + // @formatter:on + + return http.build(); + } + + static interface AuthoritiesConverter extends Converter> { + } + + @Bean + AuthoritiesConverter realmRoles2AuthoritiesConverter() { + return (Jwt jwt) -> { + 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(); + }; + } + } + + @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()); + } + + @PreAuthorize("hasRole('AUTHORIZED_PERSONNEL')") + public String getSecret() { + return "Only authorized personnel can read that"; + } + } + + @RestController + @RequiredArgsConstructor + public static class GreetingController { + private final MessageService messageService; + + @GetMapping("/greet") + public ResponseEntity greet() { + return ResponseEntity.ok(messageService.greet()); + } + + @GetMapping("/secured-route") + public ResponseEntity securedRoute() { + return ResponseEntity.ok(messageService.getSecret()); + } + + @GetMapping("/secured-method") + @PreAuthorize("hasRole('AUTHORIZED_PERSONNEL')") + public ResponseEntity securedMethod() { + return ResponseEntity.ok(messageService.getSecret()); + } + } + +} diff --git a/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/main/resources/application.properties b/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/main/resources/application.properties new file mode 100644 index 0000000000..998f6303aa --- /dev/null +++ b/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.security.oauth2.resourceserver.jwt.issuer-uri=https://localhost:8443/realms/master 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 new file mode 100644 index 0000000000..36be2df331 --- /dev/null +++ b/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/test/java/com/baeldung/MessageServiceUnitTest.java @@ -0,0 +1,79 @@ +package com.baeldung; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +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({ MessageService.class }) +@ExtendWith(SpringExtension.class) +@EnableMethodSecurity +class MessageServiceUnitTest { + @Autowired + MessageService messageService; + + /*----------------------------------------------------------------------------*/ + /* greet() */ + /* Expects a JwtAuthenticationToken to be retrieved from the security-context */ + /*----------------------------------------------------------------------------*/ + + @Test + void givenSecurityContextIsNotSet_whenGreet_thenThrowsAuthenticationCredentialsNotFoundException() { + assertThrows(AuthenticationCredentialsNotFoundException.class, () -> messageService.getSecret()); + } + + @Test + @WithAnonymousUser + void givenUserIsNotAuthenticated_whenGreet_thenThrowsAccessDeniedException() { + 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()); + } + + @Test + @WithMockUser(authorities = { "admin", "ROLE_AUTHORIZED_PERSONNEL" }, username = "ch4mpy") + void givenSecurityContextIsPopulatedWithUsernamePasswordAuthenticationToken_whenGreet_thenThrowsClassCastException() { + assertThrows(ClassCastException.class, () -> messageService.greet()); + } + + /*--------------------------------------------------------------------*/ + /* getSecret() */ + /* is secured with "@PreAuthorize("hasRole('AUTHORIZED_PERSONNEL')")" */ + /*--------------------------------------------------------------------*/ + + @Test + @WithAnonymousUser + void givenUserIsNotAuthenticated_whenGetSecret_thenThrowsAccessDeniedException() { + assertThrows(AccessDeniedException.class, () -> messageService.getSecret()); + } + + @Test + @WithMockJwtAuth(authorities = { "admin", "ROLE_AUTHORIZED_PERSONNEL" }, claims = @OpenIdClaims(preferredUsername = "ch4mpy")) + void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecret_thenReturnSecret() { + assertEquals("Only authorized personnel can read that", messageService.getSecret()); + } + + @Test + @WithMockJwtAuth(authorities = { "admin" }, claims = @OpenIdClaims(preferredUsername = "ch4mpy")) + void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecret_thenThrowsAccessDeniedException() { + assertThrows(AccessDeniedException.class, () -> messageService.getSecret()); + } + +} 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 new file mode 100644 index 0000000000..0c6504bff0 --- /dev/null +++ b/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/test/java/com/baeldung/ServletResourceServerApplicationIntegrationTest.java @@ -0,0 +1,114 @@ +package com.baeldung; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +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; + +@SpringBootTest(webEnvironment = WebEnvironment.MOCK) +@AutoConfigureMockMvc +class ServletResourceServerApplicationIntegrationTest { + @Autowired + MockMvc api; + + /*-----------------------------------------------------------------------------*/ + /* /greet */ + /* This end-point secured with ".anyRequest().authenticated()" in SecurityConf */ + /*-----------------------------------------------------------------------------*/ + + @Test + @WithAnonymousUser + void givenUserIsNotAuthenticated_whenGetGreet_thenUnauthorized() throws Exception { + // @formatter:off + api.perform(get("/greet")) + .andExpect(status().isUnauthorized()); + // @formatter:on + } + + @Test + @WithMockJwtAuth(authorities = { "admin", "ROLE_AUTHORIZED_PERSONNEL" }, claims = @OpenIdClaims(preferredUsername = "ch4mpy")) + void givenUserIsAuthenticated_whenGetGreet_thenOk() throws Exception { + // @formatter:off + api.perform(get("/greet")) + .andExpect(status().isOk()) + .andExpect(content().string("Hello ch4mpy! You are granted with [admin, ROLE_AUTHORIZED_PERSONNEL].")); + // @formatter:on + } + + /*---------------------------------------------------------------------------------------------------------------------*/ + /* /secured-route */ + /* This end-point is secured with ".requestMatchers("/secured-route").hasRole("AUTHORIZED_PERSONNEL")" in SecurityConf */ + /*---------------------------------------------------------------------------------------------------------------------*/ + + @Test + @WithAnonymousUser + void givenUserIsNotAuthenticated_whenGetSecuredRoute_thenUnauthorized() throws Exception { + // @formatter:off + api.perform(get("/secured-route")) + .andExpect(status().isUnauthorized()); + // @formatter:on + } + + @Test + @WithMockJwtAuth("ROLE_AUTHORIZED_PERSONNEL") + void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenOk() throws Exception { + // @formatter:off + api.perform(get("/secured-route")) + .andExpect(status().isOk()) + .andExpect(content().string("Only authorized personnel can read that")); + // @formatter:on + } + + @Test + @WithMockJwtAuth("admin") + void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenForbidden() throws Exception { + // @formatter:off + api.perform(get("/secured-route")) + .andExpect(status().isForbidden()); + // @formatter:on + } + + /*---------------------------------------------------------------------------------------------------------*/ + /* /secured-method */ + /* This end-point is secured with "@PreAuthorize("hasRole('AUTHORIZED_PERSONNEL')")" on @Controller method */ + /*---------------------------------------------------------------------------------------------------------*/ + + @Test + @WithAnonymousUser + void givenUserIsNotAuthenticated_whenGetSecuredMethod_thenUnauthorized() throws Exception { + // @formatter:off + api.perform(get("/secured-method")) + .andExpect(status().isUnauthorized()); + // @formatter:on + } + + @Test + @WithMockJwtAuth("ROLE_AUTHORIZED_PERSONNEL") + void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenOk() throws Exception { + // @formatter:off + api.perform(get("/secured-method")) + .andExpect(status().isOk()) + .andExpect(content().string("Only authorized personnel can read that")); + // @formatter:on + } + + @Test + @WithMockJwtAuth("admin") + void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenForbidden() throws Exception { + // @formatter:off + api.perform(get("/secured-method")) + .andExpect(status().isForbidden()); + // @formatter:on + } + +} 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 new file mode 100644 index 0000000000..76902bb9b7 --- /dev/null +++ b/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/test/java/com/baeldung/SpringAddonsGreetingControllerUnitTest.java @@ -0,0 +1,131 @@ +package com.baeldung; + +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.Test; +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.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.WithMockJwtAuth; + +@WebMvcTest(controllers = GreetingController.class, properties = { "server.ssl.enabled=false" }) +class SpringAddonsGreetingControllerUnitTest { + + @MockBean + MessageService messageService; + + @Autowired + MockMvc api; + + /*-----------------------------------------------------------------------------*/ + /* /greet */ + /* This end-point secured with ".anyRequest().authenticated()" in SecurityConf */ + /*-----------------------------------------------------------------------------*/ + + @Test + @WithAnonymousUser + void givenUserIsNotAuthenticated_whenGetGreet_thenUnauthorized() throws Exception { + // @formatter:off + api.perform(get("/greet")) + .andExpect(status().isUnauthorized()); + // @formatter:on + } + + @Test + @WithMockJwtAuth + void givenUserIsAuthenticated_whenGetGreet_thenOk() throws Exception { + final var greeting = "Whatever the service returns"; + when(messageService.greet()).thenReturn(greeting); + + // @formatter:off + api.perform(get("/greet")) + .andExpect(status().isOk()) + .andExpect(content().string(greeting)); + // @formatter:on + + verify(messageService, times(1)).greet(); + } + + /*---------------------------------------------------------------------------------------------------------------------*/ + /* /secured-route */ + /* This end-point is secured with ".requestMatchers("/secured-route").hasRole("AUTHORIZED_PERSONNEL")" in SecurityConf */ + /*---------------------------------------------------------------------------------------------------------------------*/ + + @Test + @WithAnonymousUser + void givenUserIsNotAuthenticated_whenGetSecuredRoute_thenUnauthorized() throws Exception { + // @formatter:off + api.perform(get("/secured-route")) + .andExpect(status().isUnauthorized()); + // @formatter:on + } + + @Test + @WithMockJwtAuth({ "admin", "ROLE_AUTHORIZED_PERSONNEL" }) + void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenOk() throws Exception { + final var secret = "Secret!"; + when(messageService.getSecret()).thenReturn(secret); + + // @formatter:off + api.perform(get("/secured-route")) + .andExpect(status().isOk()) + .andExpect(content().string(secret)); + // @formatter:on + } + + @Test + @WithMockJwtAuth({ "admin" }) + void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenForbidden() throws Exception { + // @formatter:off + api.perform(get("/secured-route")) + .andExpect(status().isForbidden()); + // @formatter:on + } + + /*---------------------------------------------------------------------------------------------------------*/ + /* /secured-method */ + /* This end-point is secured with "@PreAuthorize("hasRole('AUTHORIZED_PERSONNEL')")" on @Controller method */ + /*---------------------------------------------------------------------------------------------------------*/ + + @Test + @WithAnonymousUser + void givenUserIsNotAuthenticated_whenGetSecuredMethod_thenUnauthorized() throws Exception { + // @formatter:off + api.perform(get("/secured-method")) + .andExpect(status().isUnauthorized()); + // @formatter:on + } + + @Test + @WithMockJwtAuth({ "admin", "ROLE_AUTHORIZED_PERSONNEL" }) + void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenOk() throws Exception { + final var secret = "Secret!"; + when(messageService.getSecret()).thenReturn(secret); + + // @formatter:off + api.perform(get("/secured-method")) + .andExpect(status().isOk()) + .andExpect(content().string(secret)); + // @formatter:on + } + + @Test + @WithMockJwtAuth(authorities = { "admin" }) + void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenForbidden() throws Exception { + // @formatter:off + api.perform(get("/secured-method")) + .andExpect(status().isForbidden()); + // @formatter:on + } + +} diff --git a/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/test/java/com/baeldung/SpringSecurityTestGreetingControllerUnitTest.java b/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/test/java/com/baeldung/SpringSecurityTestGreetingControllerUnitTest.java new file mode 100644 index 0000000000..aa53dd7531 --- /dev/null +++ b/spring-security-modules/spring-security-oauth2-testing/servlet-resource-server/src/test/java/com/baeldung/SpringSecurityTestGreetingControllerUnitTest.java @@ -0,0 +1,124 @@ +package com.baeldung; + +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.Test; +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.authority.SimpleGrantedAuthority; +import org.springframework.test.web.servlet.MockMvc; + +import com.baeldung.ServletResourceServerApplication.GreetingController; +import com.baeldung.ServletResourceServerApplication.MessageService; + +@WebMvcTest(controllers = GreetingController.class, properties = { "server.ssl.enabled=false" }) +class SpringSecurityTestGreetingControllerUnitTest { + + @MockBean + MessageService messageService; + + @Autowired + MockMvc api; + + /*-----------------------------------------------------------------------------*/ + /* /greet */ + /* This end-point secured with ".anyRequest().authenticated()" in SecurityConf */ + /*-----------------------------------------------------------------------------*/ + + @Test + void givenUserIsNotAuthenticated_whenGetGreet_thenUnauthorized() throws Exception { + // @formatter:off + api.perform(get("/greet").with(anonymous())) + .andExpect(status().isUnauthorized()); + // @formatter:on + } + + @Test + void givenUserIsAuthenticated_whenGetGreet_thenOk() throws Exception { + final var greeting = "Whatever the service returns"; + when(messageService.greet()).thenReturn(greeting); + + // @formatter:off + api.perform(get("/greet").with(jwt())) + .andExpect(status().isOk()) + .andExpect(content().string(greeting)); + // @formatter:on + + verify(messageService, times(1)).greet(); + } + + /*---------------------------------------------------------------------------------------------------------------------*/ + /* /secured-route */ + /* This end-point is secured with ".requestMatchers("/secured-route").hasRole("AUTHORIZED_PERSONNEL")" in SecurityConf */ + /*---------------------------------------------------------------------------------------------------------------------*/ + + @Test + void givenUserIsNotAuthenticated_whenGetSecuredRoute_thenUnauthorized() throws Exception { + // @formatter:off + api.perform(get("/secured-route").with(anonymous())) + .andExpect(status().isUnauthorized()); + // @formatter:on + } + + @Test + void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenOk() throws Exception { + final var secret = "Secret!"; + when(messageService.getSecret()).thenReturn(secret); + + // @formatter:off + api.perform(get("/secured-route").with(jwt().authorities(new SimpleGrantedAuthority("ROLE_AUTHORIZED_PERSONNEL")))) + .andExpect(status().isOk()) + .andExpect(content().string(secret)); + // @formatter:on + } + + @Test + void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenForbidden() throws Exception { + // @formatter:off + api.perform(get("/secured-route").with(jwt().authorities(new SimpleGrantedAuthority("admin")))) + .andExpect(status().isForbidden()); + // @formatter:on + } + + /*---------------------------------------------------------------------------------------------------------*/ + /* /secured-method */ + /* This end-point is secured with "@PreAuthorize("hasRole('AUTHORIZED_PERSONNEL')")" on @Controller method */ + /*---------------------------------------------------------------------------------------------------------*/ + + @Test + void givenUserIsNotAuthenticated_whenGetSecuredMethod_thenUnauthorized() throws Exception { + // @formatter:off + api.perform(get("/secured-method").with(anonymous())) + .andExpect(status().isUnauthorized()); + // @formatter:on + } + + @Test + void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenOk() throws Exception { + final var secret = "Secret!"; + when(messageService.getSecret()).thenReturn(secret); + + // @formatter:off + api.perform(get("/secured-method").with(jwt().authorities(new SimpleGrantedAuthority("ROLE_AUTHORIZED_PERSONNEL")))) + .andExpect(status().isOk()) + .andExpect(content().string(secret)); + // @formatter:on + } + + @Test + void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenForbidden() throws Exception { + // @formatter:off + api.perform(get("/secured-method").with(jwt().authorities(new SimpleGrantedAuthority("admin")))) + .andExpect(status().isForbidden()); + // @formatter:on + } + +}