BAEL-7704 : (CONFIGURATION)

BAEL-7704 :

Update to latest spring-addons.

Fix conf: authentication converter was no used by the security
filter-chain.

Add docker compose file for a Keycloak instance with an imported realm.
This commit is contained in:
ch4mpy
2024-04-19 23:09:30 -10:00
parent 979f4ffb2c
commit eb4996272b
18 changed files with 2065 additions and 100 deletions
@@ -12,6 +12,7 @@
<groupId>com.baeldung</groupId>
<artifactId>spring-security-oauth2-testing</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<dependencies>
@@ -1,7 +1,5 @@
package com.baeldung;
import static org.springframework.security.config.Customizer.withDefaults;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
@@ -17,6 +15,7 @@ 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.AbstractAuthenticationToken;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
@@ -52,22 +51,22 @@ public class ReactiveResourceServerApplication {
@EnableReactiveMethodSecurity
static class SecurityConfig {
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.oauth2ResourceServer(resourceServer -> resourceServer.jwt(withDefaults()));
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, Converter<Jwt, Mono<AbstractAuthenticationToken>> authenticationConverter) {
http.oauth2ResourceServer(resourceServer -> resourceServer.jwt(jwtResourceServer -> jwtResourceServer.jwtAuthenticationConverter(authenticationConverter)));
http.securityContextRepository(NoOpServerSecurityContextRepository.getInstance());
http.csrf(CsrfSpec::disable);
http.exceptionHandling(eh -> eh
.accessDeniedHandler((var exchange, var ex) -> exchange.getPrincipal().flatMap(principal -> {
final var response = exchange.getResponse();
response.setStatusCode(
principal instanceof AnonymousAuthenticationToken ? HttpStatus.UNAUTHORIZED
: HttpStatus.FORBIDDEN);
response.getHeaders().setContentType(MediaType.TEXT_PLAIN);
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.exceptionHandling(eh -> eh.accessDeniedHandler((var exchange, var ex) -> exchange.getPrincipal()
.flatMap(principal -> {
final var response = exchange.getResponse();
response.setStatusCode(principal instanceof AnonymousAuthenticationToken ? HttpStatus.UNAUTHORIZED : HttpStatus.FORBIDDEN);
response.getHeaders()
.setContentType(MediaType.TEXT_PLAIN);
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));
})));
// @formatter:off
http.authorizeExchange(req -> req
@@ -84,17 +83,18 @@ public class ReactiveResourceServerApplication {
@Bean
ReactiveJwtAuthoritiesConverter realmRoles2AuthoritiesConverter() {
return (Jwt jwt) -> {
final var realmRoles = Optional.of(jwt.getClaimAsMap("realm_access")).orElse(Map.of());
final var realmRoles = Optional.of(jwt.getClaimAsMap("realm_access"))
.orElse(Map.of());
@SuppressWarnings("unchecked")
final var roles = (List<String>) realmRoles.getOrDefault("roles", List.of());
return Flux.fromStream(roles.stream()).map(SimpleGrantedAuthority::new)
.map(GrantedAuthority.class::cast);
return Flux.fromStream(roles.stream())
.map(SimpleGrantedAuthority::new)
.map(GrantedAuthority.class::cast);
};
}
@Bean
ReactiveJwtAuthenticationConverter authenticationConverter(
Converter<Jwt, Flux<GrantedAuthority>> authoritiesConverter) {
ReactiveJwtAuthenticationConverter authenticationConverter(Converter<Jwt, Flux<GrantedAuthority>> authoritiesConverter) {
final var authenticationConverter = new ReactiveJwtAuthenticationConverter();
authenticationConverter.setPrincipalClaimName(StandardClaimNames.PREFERRED_USERNAME);
authenticationConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
@@ -106,10 +106,12 @@ public class ReactiveResourceServerApplication {
public static class MessageService {
public Mono<String> greet() {
return ReactiveSecurityContextHolder.getContext().map(ctx -> {
final var who = (JwtAuthenticationToken) ctx.getAuthentication();
return "Hello %s! You are granted with %s.".formatted(who.getName(), who.getAuthorities());
}).switchIfEmpty(Mono.error(new AuthenticationCredentialsNotFoundException("Security context is empty")));
return ReactiveSecurityContextHolder.getContext()
.map(ctx -> {
final var who = (JwtAuthenticationToken) ctx.getAuthentication();
return "Hello %s! You are granted with %s.".formatted(who.getName(), who.getAuthorities());
})
.switchIfEmpty(Mono.error(new AuthenticationCredentialsNotFoundException("Security context is empty")));
}
@PreAuthorize("hasRole('AUTHORIZED_PERSONNEL')")
@@ -125,18 +127,21 @@ public class ReactiveResourceServerApplication {
@GetMapping("/greet")
public Mono<ResponseEntity<String>> greet() {
return messageService.greet().map(ResponseEntity::ok);
return messageService.greet()
.map(ResponseEntity::ok);
}
@GetMapping("/secured-route")
public Mono<ResponseEntity<String>> securedRoute() {
return messageService.getSecret().map(ResponseEntity::ok);
return messageService.getSecret()
.map(ResponseEntity::ok);
}
@GetMapping("/secured-method")
@PreAuthorize("hasRole('AUTHORIZED_PERSONNEL')")
public Mono<ResponseEntity<String>> securedMethod() {
return messageService.getSecret().map(ResponseEntity::ok);
return messageService.getSecret()
.map(ResponseEntity::ok);
}
}
@@ -1,6 +0,0 @@
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://localhost:8443/realms/master
@@ -0,0 +1,11 @@
server:
port: 8082
spring:
application:
name: reactive-resource-server
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:8080/realms/baeldung
@@ -0,0 +1,16 @@
${AnsiColor.GREEN} _____ _ _
${AnsiColor.GREEN} | __ \ | | (_)
${AnsiColor.GREEN} | |__) |___ __ _ ___| |_ ___ _____ _ __ ___ ___ ___ _ _ _ __ ___ ___ ___ ___ _ ____ _____ _ __
${AnsiColor.GREEN} | _ // _ \/ _` |/ __| __| \ \ / / _ \ | '__/ _ \/ __|/ _ \| | | | '__/ __/ _ \ / __|/ _ \ '__\ \ / / _ \ '__|
${AnsiColor.GREEN} | | \ \ __/ (_| | (__| |_| |\ V / __/ | | | __/\__ \ (_) | |_| | | | (_| __/ \__ \ __/ | \ V / __/ |
${AnsiColor.GREEN} |_| \_\___|\__,_|\___|\__|_| \_/ \___| |_| \___||___/\___/ \__,_|_| \___\___| |___/\___|_| \_/ \___|_|
${AnsiColor.GREEN}
${AnsiColor.GREEN}
${AnsiColor.BLUE} __ __ __ ______ __ __ ______
${AnsiColor.BLUE} _____/ /_ / // / ____ ___ ____ / ____ \_____/ // / _________ / __/ /_ _________ ____ ___
${AnsiColor.BLUE} / ___/ __ \/ // /_/ __ `__ \/ __ \/ / __ `/ ___/ // /_______/ ___/ __ \/ /_/ __// ___/ __ \/ __ `__ \
${AnsiColor.BLUE}/ /__/ / / /__ __/ / / / / / /_/ / / /_/ / /__/__ __/_____(__ ) /_/ / __/ /__/ /__/ /_/ / / / / / /
${AnsiColor.BLUE}\___/_/ /_/ /_/ /_/ /_/ /_/ .___/\ \__,_/\___/ /_/ /____/\____/_/ \__(_)___/\____/_/ /_/ /_/
${AnsiColor.BLUE} /_/ \____/
${AnsiColor.RED}Spring Boot ${spring-boot.formatted-version}
${AnsiColor.BLACK}
@@ -3,8 +3,6 @@ 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 java.util.List;
@@ -16,6 +14,7 @@ import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers;
import org.springframework.test.web.reactive.server.WebTestClient;
import com.baeldung.ReactiveResourceServerApplication.GreetingController;
@@ -40,7 +39,7 @@ class SpringSecurityTestGreetingControllerUnitTest {
@Test
void givenRequestIsAnonymous_whenGetGreet_thenUnauthorized() {
api.mutateWith(mockAuthentication(ANONYMOUS_AUTHENTICATION))
api.mutateWith(SecurityMockServerConfigurers.mockAuthentication(ANONYMOUS_AUTHENTICATION))
.get()
.uri("/greet")
.exchange()
@@ -53,7 +52,8 @@ class SpringSecurityTestGreetingControllerUnitTest {
final var greeting = "Whatever the service returns";
when(messageService.greet()).thenReturn(Mono.just(greeting));
api.mutateWith(mockJwt().authorities(List.of(new SimpleGrantedAuthority("admin"), new SimpleGrantedAuthority("ROLE_AUTHORIZED_PERSONNEL")))
api.mutateWith(SecurityMockServerConfigurers.mockJwt()
.authorities(List.of(new SimpleGrantedAuthority("admin"), new SimpleGrantedAuthority("ROLE_AUTHORIZED_PERSONNEL")))
.jwt(jwt -> jwt.claim(StandardClaimNames.PREFERRED_USERNAME, "ch4mpy")))
.get()
.uri("/greet")
@@ -73,7 +73,7 @@ class SpringSecurityTestGreetingControllerUnitTest {
@Test
void givenRequestIsAnonymous_whenGetSecuredRoute_thenUnauthorized() {
api.mutateWith(mockAuthentication(ANONYMOUS_AUTHENTICATION))
api.mutateWith(SecurityMockServerConfigurers.mockAuthentication(ANONYMOUS_AUTHENTICATION))
.get()
.uri("/secured-route")
.exchange()
@@ -86,7 +86,8 @@ class SpringSecurityTestGreetingControllerUnitTest {
final var secret = "Secret!";
when(messageService.getSecret()).thenReturn(Mono.just(secret));
api.mutateWith(mockJwt().authorities(new SimpleGrantedAuthority("ROLE_AUTHORIZED_PERSONNEL")))
api.mutateWith(SecurityMockServerConfigurers.mockJwt()
.authorities(new SimpleGrantedAuthority("ROLE_AUTHORIZED_PERSONNEL")))
.get()
.uri("/secured-route")
.exchange()
@@ -98,7 +99,8 @@ class SpringSecurityTestGreetingControllerUnitTest {
@Test
void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredRoute_thenForbidden() {
api.mutateWith(mockJwt().authorities(new SimpleGrantedAuthority("admin")))
api.mutateWith(SecurityMockServerConfigurers.mockJwt()
.authorities(new SimpleGrantedAuthority("admin")))
.get()
.uri("/secured-route")
.exchange()
@@ -113,7 +115,7 @@ class SpringSecurityTestGreetingControllerUnitTest {
@Test
void givenRequestIsAnonymous_whenGetSecuredMethod_thenUnauthorized() {
api.mutateWith(mockAuthentication(ANONYMOUS_AUTHENTICATION))
api.mutateWith(SecurityMockServerConfigurers.mockAuthentication(ANONYMOUS_AUTHENTICATION))
.get()
.uri("/secured-method")
.exchange()
@@ -126,7 +128,8 @@ class SpringSecurityTestGreetingControllerUnitTest {
final var secret = "Secret!";
when(messageService.getSecret()).thenReturn(Mono.just(secret));
api.mutateWith(mockJwt().authorities(new SimpleGrantedAuthority("ROLE_AUTHORIZED_PERSONNEL")))
api.mutateWith(SecurityMockServerConfigurers.mockJwt()
.authorities(new SimpleGrantedAuthority("ROLE_AUTHORIZED_PERSONNEL")))
.get()
.uri("/secured-method")
.exchange()
@@ -138,7 +141,8 @@ class SpringSecurityTestGreetingControllerUnitTest {
@Test
void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenForbidden() {
api.mutateWith(mockJwt().authorities(new SimpleGrantedAuthority("admin")))
api.mutateWith(SecurityMockServerConfigurers.mockJwt()
.authorities(new SimpleGrantedAuthority("admin")))
.get()
.uri("/secured-method")
.exchange()