spring-boot 3.1 and spring-addons 7.1.10 (#14902)
This commit is contained in:
parent
fdca014527
commit
8f3b5ecc2b
|
@ -14,7 +14,7 @@
|
||||||
<relativePath>../../parent-boot-3</relativePath>
|
<relativePath>../../parent-boot-3</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<properties>
|
<properties>
|
||||||
<spring-addons.version>6.1.0</spring-addons.version>
|
<spring-addons.version>7.1.10</spring-addons.version>
|
||||||
</properties>
|
</properties>
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
package com.baeldung;
|
package com.baeldung;
|
||||||
|
|
||||||
|
import static org.springframework.security.config.Customizer.withDefaults;
|
||||||
|
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
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.core.oidc.StandardClaimNames;
|
||||||
import org.springframework.security.oauth2.jwt.Jwt;
|
import org.springframework.security.oauth2.jwt.Jwt;
|
||||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
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.SecurityWebFilterChain;
|
||||||
import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository;
|
import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
@ -34,6 +36,7 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
|
@ -46,68 +49,66 @@ public class ReactiveResourceServerApplication {
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebFluxSecurity
|
@EnableWebFluxSecurity
|
||||||
@EnableReactiveMethodSecurity
|
@EnableReactiveMethodSecurity
|
||||||
public class SecurityConfig {
|
static class SecurityConfig {
|
||||||
@Bean
|
@Bean
|
||||||
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, Converter<Jwt, Mono<Collection<GrantedAuthority>>> authoritiesConverter) {
|
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||||
http.oauth2ResourceServer()
|
http.oauth2ResourceServer(resourceServer -> resourceServer.jwt(withDefaults()));
|
||||||
.jwt()
|
http.securityContextRepository(NoOpServerSecurityContextRepository.getInstance());
|
||||||
.jwtAuthenticationConverter(jwt -> authoritiesConverter.convert(jwt)
|
http.csrf(csrf -> csrf.disable());
|
||||||
.map(authorities -> new JwtAuthenticationToken(jwt, authorities)));
|
http.exceptionHandling(eh -> eh
|
||||||
http.securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
|
.accessDeniedHandler((var exchange, var ex) -> exchange.getPrincipal().flatMap(principal -> {
|
||||||
.csrf()
|
|
||||||
.disable();
|
|
||||||
http.exceptionHandling()
|
|
||||||
.accessDeniedHandler((var exchange, var ex) -> exchange.getPrincipal()
|
|
||||||
.flatMap(principal -> {
|
|
||||||
final var response = exchange.getResponse();
|
final var response = exchange.getResponse();
|
||||||
response.setStatusCode(principal instanceof AnonymousAuthenticationToken ? HttpStatus.UNAUTHORIZED : HttpStatus.FORBIDDEN);
|
response.setStatusCode(
|
||||||
response.getHeaders()
|
principal instanceof AnonymousAuthenticationToken ? HttpStatus.UNAUTHORIZED
|
||||||
.setContentType(MediaType.TEXT_PLAIN);
|
: HttpStatus.FORBIDDEN);
|
||||||
|
response.getHeaders().setContentType(MediaType.TEXT_PLAIN);
|
||||||
final var dataBufferFactory = response.bufferFactory();
|
final var dataBufferFactory = response.bufferFactory();
|
||||||
final var buffer = dataBufferFactory.wrap(ex.getMessage()
|
final var buffer = dataBufferFactory.wrap(ex.getMessage().getBytes(Charset.defaultCharset()));
|
||||||
.getBytes(Charset.defaultCharset()));
|
|
||||||
return response.writeWith(Mono.just(buffer))
|
return response.writeWith(Mono.just(buffer))
|
||||||
.doOnError(error -> DataBufferUtils.release(buffer));
|
.doOnError(error -> DataBufferUtils.release(buffer));
|
||||||
}));
|
})));
|
||||||
|
|
||||||
http.authorizeExchange()
|
// @formatter:off
|
||||||
.pathMatchers("/secured-route")
|
http.authorizeExchange(req -> req
|
||||||
.hasRole("AUTHORIZED_PERSONNEL")
|
.pathMatchers("/secured-route").hasRole("AUTHORIZED_PERSONNEL").anyExchange()
|
||||||
.anyExchange()
|
.authenticated());
|
||||||
.authenticated();
|
// @formatter:on
|
||||||
|
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
static interface AuthoritiesConverter extends Converter<Jwt, Mono<Collection<GrantedAuthority>>> {
|
static interface ReactiveJwtAuthoritiesConverter extends Converter<Jwt, Flux<GrantedAuthority>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
AuthoritiesConverter realmRoles2AuthoritiesConverter() {
|
ReactiveJwtAuthoritiesConverter realmRoles2AuthoritiesConverter() {
|
||||||
return (Jwt jwt) -> {
|
return (Jwt jwt) -> {
|
||||||
final var realmRoles = Optional.of(jwt.getClaimAsMap("realm_access"))
|
final var realmRoles = Optional.of(jwt.getClaimAsMap("realm_access")).orElse(Map.of());
|
||||||
.orElse(Map.of());
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
final var roles = (List<String>) realmRoles.getOrDefault("roles", List.of());
|
final var roles = (List<String>) realmRoles.getOrDefault("roles", List.of());
|
||||||
return Mono.just(roles.stream()
|
return Flux.fromStream(roles.stream()).map(SimpleGrantedAuthority::new)
|
||||||
.map(SimpleGrantedAuthority::new)
|
.map(GrantedAuthority.class::cast);
|
||||||
.map(GrantedAuthority.class::cast)
|
|
||||||
.toList());
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
ReactiveJwtAuthenticationConverter authenticationConverter(
|
||||||
|
Converter<Jwt, Flux<GrantedAuthority>> authoritiesConverter) {
|
||||||
|
final var authenticationConverter = new ReactiveJwtAuthenticationConverter();
|
||||||
|
authenticationConverter.setPrincipalClaimName(StandardClaimNames.PREFERRED_USERNAME);
|
||||||
|
authenticationConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
|
||||||
|
return authenticationConverter;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public static class MessageService {
|
public static class MessageService {
|
||||||
|
|
||||||
public Mono<String> greet() {
|
public Mono<String> greet() {
|
||||||
return ReactiveSecurityContextHolder.getContext()
|
return ReactiveSecurityContextHolder.getContext().map(ctx -> {
|
||||||
.map(ctx -> {
|
final var who = (JwtAuthenticationToken) ctx.getAuthentication();
|
||||||
final var who = (JwtAuthenticationToken) ctx.getAuthentication();
|
return "Hello %s! You are granted with %s.".formatted(who.getName(), who.getAuthorities());
|
||||||
final var claims = who.getTokenAttributes();
|
}).switchIfEmpty(Mono.error(new AuthenticationCredentialsNotFoundException("Security context is empty")));
|
||||||
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')")
|
@PreAuthorize("hasRole('AUTHORIZED_PERSONNEL')")
|
||||||
|
@ -118,26 +119,23 @@ public class ReactiveResourceServerApplication {
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class GreetingController {
|
public static class GreetingController {
|
||||||
private final MessageService messageService;
|
private final MessageService messageService;
|
||||||
|
|
||||||
@GetMapping("/greet")
|
@GetMapping("/greet")
|
||||||
public Mono<ResponseEntity<String>> greet() {
|
public Mono<ResponseEntity<String>> greet() {
|
||||||
return messageService.greet()
|
return messageService.greet().map(ResponseEntity::ok);
|
||||||
.map(ResponseEntity::ok);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/secured-route")
|
@GetMapping("/secured-route")
|
||||||
public Mono<ResponseEntity<String>> securedRoute() {
|
public Mono<ResponseEntity<String>> securedRoute() {
|
||||||
return messageService.getSecret()
|
return messageService.getSecret().map(ResponseEntity::ok);
|
||||||
.map(ResponseEntity::ok);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/secured-method")
|
@GetMapping("/secured-method")
|
||||||
@PreAuthorize("hasRole('AUTHORIZED_PERSONNEL')")
|
@PreAuthorize("hasRole('AUTHORIZED_PERSONNEL')")
|
||||||
public Mono<ResponseEntity<String>> securedMethod() {
|
public Mono<ResponseEntity<String>> securedMethod() {
|
||||||
return messageService.getSecret()
|
return messageService.getSecret().map(ResponseEntity::ok);
|
||||||
.map(ResponseEntity::ok);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,28 +3,49 @@ package com.baeldung;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
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.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.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.context.annotation.Import;
|
||||||
import org.springframework.security.access.AccessDeniedException;
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
|
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||||
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
|
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.WithAnonymousUser;
|
||||||
import org.springframework.security.test.context.support.WithMockUser;
|
import org.springframework.security.test.context.support.WithMockUser;
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||||
|
|
||||||
import com.baeldung.ReactiveResourceServerApplication.MessageService;
|
import com.baeldung.ReactiveResourceServerApplication.MessageService;
|
||||||
import com.c4_soft.springaddons.security.oauth2.test.annotations.OpenIdClaims;
|
import com.baeldung.ReactiveResourceServerApplication.SecurityConfig;
|
||||||
import com.c4_soft.springaddons.security.oauth2.test.annotations.WithMockJwtAuth;
|
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)
|
@ExtendWith(SpringExtension.class)
|
||||||
@EnableReactiveMethodSecurity
|
@TestInstance(Lifecycle.PER_CLASS)
|
||||||
class MessageServiceUnitTest {
|
class MessageServiceUnitTest {
|
||||||
@Autowired
|
@Autowired
|
||||||
MessageService messageService;
|
MessageService messageService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
WithJwt.AuthenticationFactory authFactory;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
ReactiveJwtDecoder jwtDecoder;
|
||||||
|
|
||||||
/*----------------------------------------------------------------------------*/
|
/*----------------------------------------------------------------------------*/
|
||||||
/* greet() */
|
/* greet() */
|
||||||
/* Expects a JwtAuthenticationToken to be retrieved from the security-context */
|
/* Expects a JwtAuthenticationToken to be retrieved from the security-context */
|
||||||
|
@ -43,10 +64,12 @@ class MessageServiceUnitTest {
|
||||||
.block());
|
.block());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@ParameterizedTest
|
||||||
@WithMockJwtAuth(authorities = { "admin", "ROLE_AUTHORIZED_PERSONNEL" }, claims = @OpenIdClaims(preferredUsername = "ch4mpy"))
|
@MethodSource("allIdentities")
|
||||||
void givenSecurityContextIsPopulatedWithJwtAuthenticationToken_whenGreet_thenReturnGreetingWithPreferredUsernameAndAuthorities() {
|
void givenUserIsAuthenticated_whenGreet_thenReturnGreetingWithPreferredUsernameAndAuthorities(@ParameterizedAuthentication Authentication auth) {
|
||||||
assertEquals("Hello ch4mpy! You are granted with [admin, ROLE_AUTHORIZED_PERSONNEL].", messageService.greet()
|
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());
|
.block());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,17 +93,25 @@ class MessageServiceUnitTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockJwtAuth(authorities = { "admin", "ROLE_AUTHORIZED_PERSONNEL" }, claims = @OpenIdClaims(preferredUsername = "ch4mpy"))
|
@WithJwt("ch4mpy.json")
|
||||||
void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecret_thenReturnSecret() {
|
void givenUserIsCh4mpy_whenGetSecret_thenReturnSecret() {
|
||||||
assertEquals("Only authorized personnel can read that", messageService.getSecret()
|
assertEquals("Only authorized personnel can read that", messageService.getSecret()
|
||||||
.block());
|
.block());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockJwtAuth(authorities = { "admin" }, claims = @OpenIdClaims(preferredUsername = "ch4mpy"))
|
@WithJwt("tonton-pirate.json")
|
||||||
void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecret_thenThrowsAccessDeniedException() {
|
void givenUserIsTontonPirate_whenGetSecret_thenThrowsAccessDeniedException() {
|
||||||
assertThrows(AccessDeniedException.class, () -> messageService.getSecret()
|
assertThrows(AccessDeniedException.class, () -> messageService.getSecret()
|
||||||
.block());
|
.block());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*--------------------------------------------*/
|
||||||
|
/* methodSource returning all test identities */
|
||||||
|
/*--------------------------------------------*/
|
||||||
|
private Stream<AbstractAuthenticationToken> allIdentities() {
|
||||||
|
final var authentications = authFactory.authenticationsFrom("ch4mpy.json", "tonton-pirate.json").toList();
|
||||||
|
return authentications.stream();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,8 @@ import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||||
import org.springframework.security.test.context.support.WithAnonymousUser;
|
import org.springframework.security.test.context.support.WithAnonymousUser;
|
||||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
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.WithJwt;
|
||||||
import com.c4_soft.springaddons.security.oauth2.test.annotations.WithMockJwtAuth;
|
import com.c4_soft.springaddons.security.oauth2.test.annotations.WithMockAuthentication;
|
||||||
|
|
||||||
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
|
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
|
||||||
@AutoConfigureWebTestClient
|
@AutoConfigureWebTestClient
|
||||||
|
@ -33,7 +33,7 @@ class ReactiveResourceServerApplicationIntegrationTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockJwtAuth(authorities = { "admin", "ROLE_AUTHORIZED_PERSONNEL" }, claims = @OpenIdClaims(preferredUsername = "ch4mpy"))
|
@WithJwt("ch4mpy.json")
|
||||||
void givenUserIsAuthenticated_whenGetGreet_thenOk() throws Exception {
|
void givenUserIsAuthenticated_whenGetGreet_thenOk() throws Exception {
|
||||||
api.get()
|
api.get()
|
||||||
.uri("/greet")
|
.uri("/greet")
|
||||||
|
@ -60,7 +60,7 @@ class ReactiveResourceServerApplicationIntegrationTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockJwtAuth("ROLE_AUTHORIZED_PERSONNEL")
|
@WithMockAuthentication("ROLE_AUTHORIZED_PERSONNEL")
|
||||||
void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenOk() throws Exception {
|
void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenOk() throws Exception {
|
||||||
api.get()
|
api.get()
|
||||||
.uri("/secured-route")
|
.uri("/secured-route")
|
||||||
|
@ -72,7 +72,7 @@ class ReactiveResourceServerApplicationIntegrationTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockJwtAuth("admin")
|
@WithMockAuthentication("admin")
|
||||||
void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenForbidden() throws Exception {
|
void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenForbidden() throws Exception {
|
||||||
api.get()
|
api.get()
|
||||||
.uri("/secured-route")
|
.uri("/secured-route")
|
||||||
|
@ -97,7 +97,7 @@ class ReactiveResourceServerApplicationIntegrationTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockJwtAuth("ROLE_AUTHORIZED_PERSONNEL")
|
@WithMockAuthentication("ROLE_AUTHORIZED_PERSONNEL")
|
||||||
void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenOk() throws Exception {
|
void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenOk() throws Exception {
|
||||||
api.get()
|
api.get()
|
||||||
.uri("/secured-method")
|
.uri("/secured-method")
|
||||||
|
@ -109,7 +109,7 @@ class ReactiveResourceServerApplicationIntegrationTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockJwtAuth("admin")
|
@WithMockAuthentication("admin")
|
||||||
void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenForbidden() throws Exception {
|
void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenForbidden() throws Exception {
|
||||||
api.get()
|
api.get()
|
||||||
.uri("/secured-method")
|
.uri("/secured-method")
|
||||||
|
|
|
@ -5,16 +5,19 @@ import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
|
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
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.security.test.context.support.WithAnonymousUser;
|
||||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
|
|
||||||
import com.baeldung.ReactiveResourceServerApplication.GreetingController;
|
import com.baeldung.ReactiveResourceServerApplication.GreetingController;
|
||||||
import com.baeldung.ReactiveResourceServerApplication.MessageService;
|
import com.baeldung.ReactiveResourceServerApplication.MessageService;
|
||||||
import com.c4_soft.springaddons.security.oauth2.test.annotations.OpenIdClaims;
|
import com.c4_soft.springaddons.security.oauth2.test.annotations.WithMockAuthentication;
|
||||||
import com.c4_soft.springaddons.security.oauth2.test.annotations.WithMockJwtAuth;
|
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;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
@ -28,115 +31,88 @@ class SpringAddonsGreetingControllerUnitTest {
|
||||||
WebTestClient api;
|
WebTestClient api;
|
||||||
|
|
||||||
/*-----------------------------------------------------------------------------*/
|
/*-----------------------------------------------------------------------------*/
|
||||||
/* /greet */
|
/* /greet */
|
||||||
/* This end-point secured with ".anyRequest().authenticated()" in SecurityConf */
|
/*
|
||||||
|
* This end-point secured with ".anyRequest().authenticated()" in SecurityConf
|
||||||
|
*/
|
||||||
/*-----------------------------------------------------------------------------*/
|
/*-----------------------------------------------------------------------------*/
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithAnonymousUser
|
@WithAnonymousUser
|
||||||
void givenRequestIsAnonymous_whenGetGreet_thenUnauthorized() throws Exception {
|
void givenRequestIsAnonymous_whenGetGreet_thenUnauthorized() throws Exception {
|
||||||
api.get()
|
api.get().uri("/greet").exchange().expectStatus().isUnauthorized();
|
||||||
.uri("/greet")
|
|
||||||
.exchange()
|
|
||||||
.expectStatus()
|
|
||||||
.isUnauthorized();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@ParameterizedTest
|
||||||
@WithMockJwtAuth(authorities = { "admin", "ROLE_AUTHORIZED_PERSONNEL" }, claims = @OpenIdClaims(preferredUsername = "ch4mpy"))
|
@AuthenticationSource({
|
||||||
void givenUserIsAuthenticated_whenGetGreet_thenOk() throws Exception {
|
@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";
|
final var greeting = "Whatever the service returns";
|
||||||
when(messageService.greet()).thenReturn(Mono.just(greeting));
|
when(messageService.greet()).thenReturn(Mono.just(greeting));
|
||||||
|
|
||||||
api.get()
|
api.get().uri("/greet").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo(greeting);
|
||||||
.uri("/greet")
|
|
||||||
.exchange()
|
|
||||||
.expectStatus()
|
|
||||||
.isOk()
|
|
||||||
.expectBody(String.class)
|
|
||||||
.isEqualTo(greeting);
|
|
||||||
|
|
||||||
verify(messageService, times(1)).greet();
|
verify(messageService, times(1)).greet();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*---------------------------------------------------------------------------------------------------------------------*/
|
/*---------------------------------------------------------------------------------------------------------------------*/
|
||||||
/* /secured-route */
|
/* /secured-route */
|
||||||
/* This end-point is secured with ".requestMatchers("/secured-route").hasRole("AUTHORIZED_PERSONNEL")" in SecurityConf */
|
/*
|
||||||
|
* This end-point is secured with
|
||||||
|
* ".requestMatchers("/secured-route").hasRole("AUTHORIZED_PERSONNEL")" in
|
||||||
|
* SecurityConf
|
||||||
|
*/
|
||||||
/*---------------------------------------------------------------------------------------------------------------------*/
|
/*---------------------------------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithAnonymousUser
|
@WithAnonymousUser
|
||||||
void givenRequestIsAnonymous_whenGetSecuredRoute_thenUnauthorized() throws Exception {
|
void givenRequestIsAnonymous_whenGetSecuredRoute_thenUnauthorized() throws Exception {
|
||||||
api.get()
|
api.get().uri("/secured-route").exchange().expectStatus().isUnauthorized();
|
||||||
.uri("/secured-route")
|
|
||||||
.exchange()
|
|
||||||
.expectStatus()
|
|
||||||
.isUnauthorized();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockJwtAuth("ROLE_AUTHORIZED_PERSONNEL")
|
@WithMockAuthentication("ROLE_AUTHORIZED_PERSONNEL")
|
||||||
void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenOk() throws Exception {
|
void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenOk() throws Exception {
|
||||||
final var secret = "Secret!";
|
final var secret = "Secret!";
|
||||||
when(messageService.getSecret()).thenReturn(Mono.just(secret));
|
when(messageService.getSecret()).thenReturn(Mono.just(secret));
|
||||||
|
|
||||||
api.get()
|
api.get().uri("/secured-route").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo(secret);
|
||||||
.uri("/secured-route")
|
|
||||||
.exchange()
|
|
||||||
.expectStatus()
|
|
||||||
.isOk()
|
|
||||||
.expectBody(String.class)
|
|
||||||
.isEqualTo(secret);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockJwtAuth("admin")
|
@WithMockAuthentication("admin")
|
||||||
void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenForbidden() throws Exception {
|
void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenForbidden() throws Exception {
|
||||||
api.get()
|
api.get().uri("/secured-route").exchange().expectStatus().isForbidden();
|
||||||
.uri("/secured-route")
|
|
||||||
.exchange()
|
|
||||||
.expectStatus()
|
|
||||||
.isForbidden();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*---------------------------------------------------------------------------------------------------------*/
|
/*---------------------------------------------------------------------------------------------------------*/
|
||||||
/* /secured-method */
|
/* /secured-method */
|
||||||
/* This end-point is secured with "@PreAuthorize("hasRole('AUTHORIZED_PERSONNEL')")" on @Controller method */
|
/*
|
||||||
|
* This end-point is secured with
|
||||||
|
* "@PreAuthorize("hasRole('AUTHORIZED_PERSONNEL')")" on @Controller method
|
||||||
|
*/
|
||||||
/*---------------------------------------------------------------------------------------------------------*/
|
/*---------------------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithAnonymousUser
|
@WithAnonymousUser
|
||||||
void givenRequestIsAnonymous_whenGetSecuredMethod_thenUnauthorized() throws Exception {
|
void givenRequestIsAnonymous_whenGetSecuredMethod_thenUnauthorized() throws Exception {
|
||||||
api.get()
|
api.get().uri("/secured-method").exchange().expectStatus().isUnauthorized();
|
||||||
.uri("/secured-method")
|
|
||||||
.exchange()
|
|
||||||
.expectStatus()
|
|
||||||
.isUnauthorized();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockJwtAuth("ROLE_AUTHORIZED_PERSONNEL")
|
@WithMockAuthentication("ROLE_AUTHORIZED_PERSONNEL")
|
||||||
void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenOk() throws Exception {
|
void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenOk() throws Exception {
|
||||||
final var secret = "Secret!";
|
final var secret = "Secret!";
|
||||||
when(messageService.getSecret()).thenReturn(Mono.just(secret));
|
when(messageService.getSecret()).thenReturn(Mono.just(secret));
|
||||||
|
|
||||||
api.get()
|
api.get().uri("/secured-method").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo(secret);
|
||||||
.uri("/secured-method")
|
|
||||||
.exchange()
|
|
||||||
.expectStatus()
|
|
||||||
.isOk()
|
|
||||||
.expectBody(String.class)
|
|
||||||
.isEqualTo(secret);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockJwtAuth("admin")
|
@WithMockAuthentication("admin")
|
||||||
void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenForbidden() throws Exception {
|
void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenForbidden() throws Exception {
|
||||||
api.get()
|
api.get().uri("/secured-method").exchange().expectStatus().isForbidden();
|
||||||
.uri("/secured-method")
|
|
||||||
.exchange()
|
|
||||||
.expectStatus()
|
|
||||||
.isForbidden();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"iss": "https://localhost:8443/realms/master",
|
||||||
|
"sub": "281c4558-550c-413b-9972-2d2e5bde6b9b",
|
||||||
|
"iat": 1695992542,
|
||||||
|
"exp": 1695992642,
|
||||||
|
"preferred_username": "ch4mpy",
|
||||||
|
"realm_access": {
|
||||||
|
"roles": [
|
||||||
|
"admin",
|
||||||
|
"ROLE_AUTHORIZED_PERSONNEL"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"email": "ch4mp@c4-soft.com",
|
||||||
|
"scope": "openid email"
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"iss": "https://localhost:8443/realms/master",
|
||||||
|
"sub": "2d2e5bde6b9b-550c-413b-9972-281c4558",
|
||||||
|
"iat": 1695992551,
|
||||||
|
"exp": 1695992651,
|
||||||
|
"preferred_username": "tonton-pirate",
|
||||||
|
"realm_access": {
|
||||||
|
"roles": [
|
||||||
|
"uncle",
|
||||||
|
"PIRATE"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"email": "tonton-pirate@c4-soft.com",
|
||||||
|
"scope": "openid email"
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
package com.baeldung;
|
package com.baeldung;
|
||||||
|
|
||||||
|
import static org.springframework.security.config.Customizer.withDefaults;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
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.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
|
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
|
||||||
import org.springframework.security.oauth2.jwt.Jwt;
|
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.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
@ -43,56 +47,52 @@ public class ServletResourceServerApplication {
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
static class SecurityConf {
|
static class SecurityConf {
|
||||||
@Bean
|
@Bean
|
||||||
SecurityFilterChain filterChain(HttpSecurity http, Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter) throws Exception {
|
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
http.oauth2ResourceServer()
|
http.oauth2ResourceServer(resourceServer -> resourceServer.jwt(withDefaults()));
|
||||||
.jwt()
|
http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
|
||||||
.jwtAuthenticationConverter(jwt -> new JwtAuthenticationToken(jwt, authoritiesConverter.convert(jwt)));
|
http.csrf(csrf -> csrf.disable());
|
||||||
http.sessionManagement()
|
http.exceptionHandling(eh -> eh.authenticationEntryPoint((request, response, authException) -> {
|
||||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
response.addHeader(HttpHeaders.WWW_AUTHENTICATE, "Bearer realm=\"Restricted Content\"");
|
||||||
.and()
|
response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
|
||||||
.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()
|
// @formatter:off
|
||||||
.requestMatchers("/secured-route")
|
http.authorizeHttpRequests(req -> req
|
||||||
.hasRole("AUTHORIZED_PERSONNEL")
|
.requestMatchers(new AntPathRequestMatcher("/secured-route")).hasRole("AUTHORIZED_PERSONNEL")
|
||||||
.anyRequest()
|
.anyRequest().authenticated());
|
||||||
.authenticated();
|
// @formatter:on
|
||||||
|
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
static interface AuthoritiesConverter extends Converter<Jwt, Collection<GrantedAuthority>> {
|
static interface JwtAuthoritiesConverter extends Converter<Jwt, Collection<GrantedAuthority>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
AuthoritiesConverter realmRoles2AuthoritiesConverter() {
|
JwtAuthoritiesConverter realmRoles2AuthoritiesConverter() {
|
||||||
return (Jwt jwt) -> {
|
return (Jwt jwt) -> {
|
||||||
final var realmRoles = Optional.of(jwt.getClaimAsMap("realm_access"))
|
final var realmRoles = Optional.of(jwt.getClaimAsMap("realm_access")).orElse(Map.of());
|
||||||
.orElse(Map.of());
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
final var roles = (List<String>) realmRoles.getOrDefault("roles", List.of());
|
final var roles = (List<String>) realmRoles.getOrDefault("roles", List.of());
|
||||||
return roles.stream()
|
return roles.stream().map(SimpleGrantedAuthority::new).map(GrantedAuthority.class::cast).toList();
|
||||||
.map(SimpleGrantedAuthority::new)
|
|
||||||
.map(GrantedAuthority.class::cast)
|
|
||||||
.toList();
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
JwtAuthenticationConverter authenticationConverter(Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter) {
|
||||||
|
final var authenticationConverter = new JwtAuthenticationConverter();
|
||||||
|
authenticationConverter.setPrincipalClaimName(StandardClaimNames.PREFERRED_USERNAME);
|
||||||
|
authenticationConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
|
||||||
|
return authenticationConverter;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public static class MessageService {
|
public static class MessageService {
|
||||||
|
|
||||||
public String greet() {
|
public String greet() {
|
||||||
final var who = (JwtAuthenticationToken) SecurityContextHolder.getContext()
|
final var who = (JwtAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
|
||||||
.getAuthentication();
|
return "Hello %s! You are granted with %s.".formatted(who.getName(), who.getAuthorities());
|
||||||
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')")
|
@PreAuthorize("hasRole('AUTHORIZED_PERSONNEL')")
|
||||||
|
|
|
@ -3,28 +3,49 @@ package com.baeldung;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
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.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.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.context.annotation.Import;
|
||||||
import org.springframework.security.access.AccessDeniedException;
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
|
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||||
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
|
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.WithAnonymousUser;
|
||||||
import org.springframework.security.test.context.support.WithMockUser;
|
import org.springframework.security.test.context.support.WithMockUser;
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||||
|
|
||||||
import com.baeldung.ServletResourceServerApplication.MessageService;
|
import com.baeldung.ServletResourceServerApplication.MessageService;
|
||||||
import com.c4_soft.springaddons.security.oauth2.test.annotations.OpenIdClaims;
|
import com.baeldung.ServletResourceServerApplication.SecurityConf;
|
||||||
import com.c4_soft.springaddons.security.oauth2.test.annotations.WithMockJwtAuth;
|
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)
|
@ExtendWith(SpringExtension.class)
|
||||||
@EnableMethodSecurity
|
@TestInstance(Lifecycle.PER_CLASS)
|
||||||
class MessageServiceUnitTest {
|
class MessageServiceUnitTest {
|
||||||
@Autowired
|
@Autowired
|
||||||
MessageService messageService;
|
MessageService messageService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
WithJwt.AuthenticationFactory authFactory;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
JwtDecoder jwtDecoder;
|
||||||
|
|
||||||
/*----------------------------------------------------------------------------*/
|
/*----------------------------------------------------------------------------*/
|
||||||
/* greet() */
|
/* greet() */
|
||||||
/* Expects a JwtAuthenticationToken to be retrieved from the security-context */
|
/* Expects a JwtAuthenticationToken to be retrieved from the security-context */
|
||||||
|
@ -41,10 +62,12 @@ class MessageServiceUnitTest {
|
||||||
assertThrows(AccessDeniedException.class, () -> messageService.getSecret());
|
assertThrows(AccessDeniedException.class, () -> messageService.getSecret());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@ParameterizedTest
|
||||||
@WithMockJwtAuth(authorities = { "admin", "ROLE_AUTHORIZED_PERSONNEL" }, claims = @OpenIdClaims(preferredUsername = "ch4mpy"))
|
@MethodSource("allIdentities")
|
||||||
void givenSecurityContextIsPopulatedWithJwtAuthenticationToken_whenGreet_thenReturnGreetingWithPreferredUsernameAndAuthorities() {
|
void givenUserIsAuthenticated_whenGreet_thenReturnGreetingWithPreferredUsernameAndAuthorities(@ParameterizedAuthentication Authentication auth) {
|
||||||
assertEquals("Hello ch4mpy! You are granted with [admin, ROLE_AUTHORIZED_PERSONNEL].", messageService.greet());
|
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
|
@Test
|
||||||
|
@ -65,15 +88,22 @@ class MessageServiceUnitTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockJwtAuth(authorities = { "admin", "ROLE_AUTHORIZED_PERSONNEL" }, claims = @OpenIdClaims(preferredUsername = "ch4mpy"))
|
@WithJwt("ch4mpy.json")
|
||||||
void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecret_thenReturnSecret() {
|
void givenUserIsCh4mpy_whenGetSecret_thenReturnSecret() {
|
||||||
assertEquals("Only authorized personnel can read that", messageService.getSecret());
|
assertEquals("Only authorized personnel can read that", messageService.getSecret());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockJwtAuth(authorities = { "admin" }, claims = @OpenIdClaims(preferredUsername = "ch4mpy"))
|
@WithJwt("tonton-pirate.json")
|
||||||
void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecret_thenThrowsAccessDeniedException() {
|
void givenUserIsTontonPirate_whenGetSecret_thenThrowsAccessDeniedException() {
|
||||||
assertThrows(AccessDeniedException.class, () -> messageService.getSecret());
|
assertThrows(AccessDeniedException.class, () -> messageService.getSecret());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*--------------------------------------------*/
|
||||||
|
/* methodSource returning all test identities */
|
||||||
|
/*--------------------------------------------*/
|
||||||
|
private Stream<AbstractAuthenticationToken> allIdentities() {
|
||||||
|
final var authentications = authFactory.authenticationsFrom("ch4mpy.json", "tonton-pirate.json").toList();
|
||||||
|
return authentications.stream();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,8 @@ import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||||
import org.springframework.security.test.context.support.WithAnonymousUser;
|
import org.springframework.security.test.context.support.WithAnonymousUser;
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
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.WithJwt;
|
||||||
import com.c4_soft.springaddons.security.oauth2.test.annotations.WithMockJwtAuth;
|
import com.c4_soft.springaddons.security.oauth2.test.annotations.WithMockAuthentication;
|
||||||
|
|
||||||
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
|
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
|
||||||
@AutoConfigureMockMvc
|
@AutoConfigureMockMvc
|
||||||
|
@ -34,7 +34,7 @@ class ServletResourceServerApplicationIntegrationTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockJwtAuth(authorities = { "admin", "ROLE_AUTHORIZED_PERSONNEL" }, claims = @OpenIdClaims(preferredUsername = "ch4mpy"))
|
@WithJwt("ch4mpy.json")
|
||||||
void givenUserIsAuthenticated_whenGetGreet_thenOk() throws Exception {
|
void givenUserIsAuthenticated_whenGetGreet_thenOk() throws Exception {
|
||||||
api.perform(get("/greet"))
|
api.perform(get("/greet"))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
|
@ -54,7 +54,7 @@ class ServletResourceServerApplicationIntegrationTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockJwtAuth("ROLE_AUTHORIZED_PERSONNEL")
|
@WithMockAuthentication("ROLE_AUTHORIZED_PERSONNEL")
|
||||||
void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenOk() throws Exception {
|
void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenOk() throws Exception {
|
||||||
api.perform(get("/secured-route"))
|
api.perform(get("/secured-route"))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
|
@ -62,7 +62,7 @@ class ServletResourceServerApplicationIntegrationTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockJwtAuth("admin")
|
@WithMockAuthentication("admin")
|
||||||
void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenForbidden() throws Exception {
|
void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenForbidden() throws Exception {
|
||||||
api.perform(get("/secured-route"))
|
api.perform(get("/secured-route"))
|
||||||
.andExpect(status().isForbidden());
|
.andExpect(status().isForbidden());
|
||||||
|
@ -81,7 +81,7 @@ class ServletResourceServerApplicationIntegrationTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockJwtAuth("ROLE_AUTHORIZED_PERSONNEL")
|
@WithMockAuthentication("ROLE_AUTHORIZED_PERSONNEL")
|
||||||
void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenOk() throws Exception {
|
void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenOk() throws Exception {
|
||||||
api.perform(get("/secured-method"))
|
api.perform(get("/secured-method"))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
|
@ -89,7 +89,7 @@ class ServletResourceServerApplicationIntegrationTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockJwtAuth("admin")
|
@WithMockAuthentication("admin")
|
||||||
void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenForbidden() throws Exception {
|
void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenForbidden() throws Exception {
|
||||||
api.perform(get("/secured-method"))
|
api.perform(get("/secured-method"))
|
||||||
.andExpect(status().isForbidden());
|
.andExpect(status().isForbidden());
|
||||||
|
|
|
@ -8,16 +8,19 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
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.security.test.context.support.WithAnonymousUser;
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
|
||||||
import com.baeldung.ServletResourceServerApplication.GreetingController;
|
import com.baeldung.ServletResourceServerApplication.GreetingController;
|
||||||
import com.baeldung.ServletResourceServerApplication.MessageService;
|
import com.baeldung.ServletResourceServerApplication.MessageService;
|
||||||
import com.c4_soft.springaddons.security.oauth2.test.annotations.OpenIdClaims;
|
import com.c4_soft.springaddons.security.oauth2.test.annotations.WithMockAuthentication;
|
||||||
import com.c4_soft.springaddons.security.oauth2.test.annotations.WithMockJwtAuth;
|
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" })
|
@WebMvcTest(controllers = GreetingController.class, properties = { "server.ssl.enabled=false" })
|
||||||
class SpringAddonsGreetingControllerUnitTest {
|
class SpringAddonsGreetingControllerUnitTest {
|
||||||
|
@ -40,9 +43,11 @@ class SpringAddonsGreetingControllerUnitTest {
|
||||||
.andExpect(status().isUnauthorized());
|
.andExpect(status().isUnauthorized());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@ParameterizedTest
|
||||||
@WithMockJwtAuth(authorities = { "admin", "ROLE_AUTHORIZED_PERSONNEL" }, claims = @OpenIdClaims(preferredUsername = "ch4mpy"))
|
@AuthenticationSource({
|
||||||
void givenUserIsAuthenticated_whenGetGreet_thenOk() throws Exception {
|
@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";
|
final var greeting = "Whatever the service returns";
|
||||||
when(messageService.greet()).thenReturn(greeting);
|
when(messageService.greet()).thenReturn(greeting);
|
||||||
|
|
||||||
|
@ -66,7 +71,7 @@ class SpringAddonsGreetingControllerUnitTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockJwtAuth({ "admin", "ROLE_AUTHORIZED_PERSONNEL" })
|
@WithMockAuthentication({ "admin", "ROLE_AUTHORIZED_PERSONNEL" })
|
||||||
void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenOk() throws Exception {
|
void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenOk() throws Exception {
|
||||||
final var secret = "Secret!";
|
final var secret = "Secret!";
|
||||||
when(messageService.getSecret()).thenReturn(secret);
|
when(messageService.getSecret()).thenReturn(secret);
|
||||||
|
@ -77,7 +82,7 @@ class SpringAddonsGreetingControllerUnitTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockJwtAuth({ "admin" })
|
@WithMockAuthentication({ "admin" })
|
||||||
void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenForbidden() throws Exception {
|
void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenForbidden() throws Exception {
|
||||||
api.perform(get("/secured-route"))
|
api.perform(get("/secured-route"))
|
||||||
.andExpect(status().isForbidden());
|
.andExpect(status().isForbidden());
|
||||||
|
@ -96,7 +101,7 @@ class SpringAddonsGreetingControllerUnitTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockJwtAuth({ "admin", "ROLE_AUTHORIZED_PERSONNEL" })
|
@WithMockAuthentication({ "admin", "ROLE_AUTHORIZED_PERSONNEL" })
|
||||||
void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenOk() throws Exception {
|
void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenOk() throws Exception {
|
||||||
final var secret = "Secret!";
|
final var secret = "Secret!";
|
||||||
when(messageService.getSecret()).thenReturn(secret);
|
when(messageService.getSecret()).thenReturn(secret);
|
||||||
|
@ -107,7 +112,7 @@ class SpringAddonsGreetingControllerUnitTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockJwtAuth(authorities = { "admin" })
|
@WithMockAuthentication({ "admin" })
|
||||||
void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenForbidden() throws Exception {
|
void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenForbidden() throws Exception {
|
||||||
api.perform(get("/secured-method"))
|
api.perform(get("/secured-method"))
|
||||||
.andExpect(status().isForbidden());
|
.andExpect(status().isForbidden());
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"iss": "https://localhost:8443/realms/master",
|
||||||
|
"sub": "281c4558-550c-413b-9972-2d2e5bde6b9b",
|
||||||
|
"iat": 1695992542,
|
||||||
|
"exp": 1695992642,
|
||||||
|
"preferred_username": "ch4mpy",
|
||||||
|
"realm_access": {
|
||||||
|
"roles": [
|
||||||
|
"admin",
|
||||||
|
"ROLE_AUTHORIZED_PERSONNEL"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"email": "ch4mp@c4-soft.com",
|
||||||
|
"scope": "openid email"
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"iss": "https://localhost:8443/realms/master",
|
||||||
|
"sub": "2d2e5bde6b9b-550c-413b-9972-281c4558",
|
||||||
|
"iat": 1695992551,
|
||||||
|
"exp": 1695992651,
|
||||||
|
"preferred_username": "tonton-pirate",
|
||||||
|
"realm_access": {
|
||||||
|
"roles": [
|
||||||
|
"uncle",
|
||||||
|
"PIRATE"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"email": "tonton-pirate@c4-soft.com",
|
||||||
|
"scope": "openid email"
|
||||||
|
}
|
Loading…
Reference in New Issue