diff --git a/pom.xml b/pom.xml
index 2873108..b45c308 100644
--- a/pom.xml
+++ b/pom.xml
@@ -14,14 +14,14 @@
org.springframework.boot
spring-boot-starter-parent
- 2.5.0
+ 3.1.3
UTF-8
UTF-8
- 11
+ 17
0.11.2
@@ -62,11 +62,21 @@
spring-boot-devtools
runtime
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
org.springframework.boot
spring-boot-starter-test
test
+
+ org.springframework.security
+ spring-security-test
+ test
+
io.projectreactor
reactor-test
diff --git a/src/main/java/com/ard333/springbootwebfluxjjwt/rest/ResourceREST.java b/src/main/java/com/ard333/springbootwebfluxjjwt/rest/ResourceREST.java
index 96c4407..180a2fd 100644
--- a/src/main/java/com/ard333/springbootwebfluxjjwt/rest/ResourceREST.java
+++ b/src/main/java/com/ard333/springbootwebfluxjjwt/rest/ResourceREST.java
@@ -1,28 +1,32 @@
package com.ard333.springbootwebfluxjjwt.rest;
-import com.ard333.springbootwebfluxjjwt.model.Message;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
+
+import com.ard333.springbootwebfluxjjwt.model.Message;
+
import reactor.core.publisher.Mono;
@RestController
+@RequestMapping("/resource")
public class ResourceREST {
- @GetMapping("/resource/user")
+ @GetMapping("/user")
@PreAuthorize("hasRole('USER')")
public Mono> user() {
return Mono.just(ResponseEntity.ok(new Message("Content for user")));
}
- @GetMapping("/resource/admin")
+ @GetMapping("/admin")
@PreAuthorize("hasRole('ADMIN')")
public Mono> admin() {
return Mono.just(ResponseEntity.ok(new Message("Content for admin")));
}
- @GetMapping("/resource/user-or-admin")
+ @GetMapping("/user-or-admin")
@PreAuthorize("hasRole('USER') or hasRole('ADMIN')")
public Mono> userOrAdmin() {
return Mono.just(ResponseEntity.ok(new Message("Content for user or admin")));
diff --git a/src/main/java/com/ard333/springbootwebfluxjjwt/security/JWTUtil.java b/src/main/java/com/ard333/springbootwebfluxjjwt/security/JWTUtil.java
index 41f08ac..13b5397 100644
--- a/src/main/java/com/ard333/springbootwebfluxjjwt/security/JWTUtil.java
+++ b/src/main/java/com/ard333/springbootwebfluxjjwt/security/JWTUtil.java
@@ -1,19 +1,19 @@
package com.ard333.springbootwebfluxjjwt.security;
-import com.ard333.springbootwebfluxjjwt.model.User;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
-import javax.annotation.PostConstruct;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import com.ard333.springbootwebfluxjjwt.model.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
-
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.stereotype.Component;
+import jakarta.annotation.PostConstruct;
@Component
public class JWTUtil {
diff --git a/src/main/java/com/ard333/springbootwebfluxjjwt/security/WebSecurityConfig.java b/src/main/java/com/ard333/springbootwebfluxjjwt/security/WebSecurityConfig.java
index 41c4667..07a2564 100644
--- a/src/main/java/com/ard333/springbootwebfluxjjwt/security/WebSecurityConfig.java
+++ b/src/main/java/com/ard333/springbootwebfluxjjwt/security/WebSecurityConfig.java
@@ -1,6 +1,7 @@
package com.ard333.springbootwebfluxjjwt.security;
import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
@@ -14,6 +15,7 @@
@AllArgsConstructor
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
+@Configuration
public class WebSecurityConfig {
private AuthenticationManager authenticationManager;
@@ -22,21 +24,20 @@ public class WebSecurityConfig {
@Bean
public SecurityWebFilterChain securitygWebFilterChain(ServerHttpSecurity http) {
return http
- .exceptionHandling()
- .authenticationEntryPoint((swe, e) ->
- Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED))
- ).accessDeniedHandler((swe, e) ->
- Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN))
- ).and()
- .csrf().disable()
- .formLogin().disable()
- .httpBasic().disable()
+ .exceptionHandling(handling -> handling
+ .authenticationEntryPoint((swe, e) ->
+ Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED))
+ ).accessDeniedHandler((swe, e) ->
+ Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN))
+ ))
+ .csrf(csrf -> csrf.disable())
+ .formLogin(formLogin -> formLogin.disable())
+ .httpBasic(httpBasic -> httpBasic.disable())
.authenticationManager(authenticationManager)
.securityContextRepository(securityContextRepository)
- .authorizeExchange()
- .pathMatchers(HttpMethod.OPTIONS).permitAll()
- .pathMatchers("/login").permitAll()
- .anyExchange().authenticated()
- .and().build();
+ .authorizeExchange(exchange -> exchange
+ .pathMatchers(HttpMethod.OPTIONS).permitAll()
+ .pathMatchers("/login").permitAll()
+ .anyExchange().authenticated()).build();
}
}
diff --git a/src/main/java/com/ard333/springbootwebfluxjjwt/security/model/AuthRequest.java b/src/main/java/com/ard333/springbootwebfluxjjwt/security/model/AuthRequest.java
index 9b01930..c14b202 100644
--- a/src/main/java/com/ard333/springbootwebfluxjjwt/security/model/AuthRequest.java
+++ b/src/main/java/com/ard333/springbootwebfluxjjwt/security/model/AuthRequest.java
@@ -1,6 +1,7 @@
package com.ard333.springbootwebfluxjjwt.security.model;
import lombok.AllArgsConstructor;
+import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@@ -9,8 +10,12 @@
*
* @author ard333
*/
-@Data @NoArgsConstructor @AllArgsConstructor @ToString
-public class AuthRequest {
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@ToString
+@Builder(toBuilder = true)
+public class AuthRequest {
private String username;
private String password;
diff --git a/src/main/java/com/ard333/springbootwebfluxjjwt/service/UserService.java b/src/main/java/com/ard333/springbootwebfluxjjwt/service/UserService.java
index 38a43ca..fce91d6 100644
--- a/src/main/java/com/ard333/springbootwebfluxjjwt/service/UserService.java
+++ b/src/main/java/com/ard333/springbootwebfluxjjwt/service/UserService.java
@@ -1,15 +1,15 @@
package com.ard333.springbootwebfluxjjwt.service;
-import com.ard333.springbootwebfluxjjwt.model.User;
-import com.ard333.springbootwebfluxjjwt.model.security.Role;
-
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
-import javax.annotation.PostConstruct;
-
import org.springframework.stereotype.Service;
+
+import com.ard333.springbootwebfluxjjwt.model.User;
+import com.ard333.springbootwebfluxjjwt.model.security.Role;
+
+import jakarta.annotation.PostConstruct;
import reactor.core.publisher.Mono;
/**
diff --git a/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/src/main/resources/META-INF/additional-spring-configuration-metadata.json
new file mode 100644
index 0000000..e5fd518
--- /dev/null
+++ b/src/main/resources/META-INF/additional-spring-configuration-metadata.json
@@ -0,0 +1,29 @@
+{
+ "properties": [
+ {
+ "name": "springbootwebfluxjjwt.password.encoder.secret",
+ "type": "java.lang.String",
+ "description": "Encoder secret"
+ },
+ {
+ "name": "springbootwebfluxjjwt.password.encoder.iteration",
+ "type": "java.lang.Integer",
+ "description": "Iteration numbers for generating the key"
+ },
+ {
+ "name": "springbootwebfluxjjwt.password.encoder.keylength",
+ "type": "java.lang.Integer",
+ "description": "The key length'"
+ },
+ {
+ "name": "springbootwebfluxjjwt.jjwt.secret",
+ "type": "java.lang.String",
+ "description": "This is secret for JWTHS512 signature algorithm that MUST have 64 byte length"
+ },
+ {
+ "name": "springbootwebfluxjjwt.jjwt.expiration",
+ "type": "java.lang.String",
+ "description": "Expiration time for token in seconds"
+ }
+ ]
+}
diff --git a/src/test/java/com/ard333/springbootwebfluxjjwt/SpringBootWebfluxJjwtApplicationTest.java b/src/test/java/com/ard333/springbootwebfluxjjwt/SpringBootWebfluxJjwtApplicationTest.java
new file mode 100644
index 0000000..4439841
--- /dev/null
+++ b/src/test/java/com/ard333/springbootwebfluxjjwt/SpringBootWebfluxJjwtApplicationTest.java
@@ -0,0 +1,26 @@
+package com.ard333.springbootwebfluxjjwt;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import com.ard333.springbootwebfluxjjwt.rest.AuthenticationREST;
+import com.ard333.springbootwebfluxjjwt.rest.ResourceREST;
+
+@SpringBootTest
+class SpringBootWebfluxJjwtApplicationTest {
+
+ @Autowired
+ AuthenticationREST authenticationREST;
+
+ @Autowired
+ ResourceREST resourceREST;
+
+ @Test
+ void contextLoads() {
+ Assertions.assertThat(authenticationREST).isNotNull();
+ Assertions.assertThat(resourceREST).isNotNull();
+ }
+
+}
diff --git a/src/test/java/com/ard333/springbootwebfluxjjwt/rest/LoginTest.java b/src/test/java/com/ard333/springbootwebfluxjjwt/rest/LoginTest.java
new file mode 100644
index 0000000..637ba7b
--- /dev/null
+++ b/src/test/java/com/ard333/springbootwebfluxjjwt/rest/LoginTest.java
@@ -0,0 +1,55 @@
+package com.ard333.springbootwebfluxjjwt.rest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.springframework.http.HttpStatusCode;
+
+import com.ard333.springbootwebfluxjjwt.model.security.AuthResponse;
+import com.ard333.springbootwebfluxjjwt.security.model.AuthRequest;
+import com.ard333.springbootwebfluxjjwt.utils.BaseRestTest;
+
+public class LoginTest extends BaseRestTest {
+
+ @Test
+ public void whenLoginWithBadPassword_shouldRespondWith4xx() {
+
+ webClient()
+ .post()
+ .uri("/login")
+ .bodyValue(AuthRequest.builder()
+ .username("user")
+ .password("wrong")
+ .build())
+ .exchangeToMono(res -> {
+ assertThat(res.statusCode()).isEqualTo(HttpStatusCode.valueOf(401));
+ return res.bodyToMono(AuthRequest.class);
+ })
+ .doOnError(e -> Assertions.fail("should not throw", e))
+ .block();
+
+ }
+
+ @Test
+ public void whenLoginWithUser_shouldRespondWith2xx() {
+ webClient()
+ .post()
+ .uri("/login")
+ .bodyValue(AuthRequest.builder()
+ .username("user")
+ .password("user")
+ .build())
+ .exchangeToMono(res -> {
+
+ assertThat(res.statusCode().is2xxSuccessful()).isTrue();
+ return res.bodyToMono(AuthResponse.class);
+
+ })
+ .doOnError(e -> Assertions.fail("should not throw", e))
+ .doOnSuccess(authRes -> {
+ assertThat(authRes.getToken()).isNotEmpty();
+ })
+ .block();
+ }
+}
diff --git a/src/test/java/com/ard333/springbootwebfluxjjwt/rest/ResourceAccessTest.java b/src/test/java/com/ard333/springbootwebfluxjjwt/rest/ResourceAccessTest.java
new file mode 100644
index 0000000..f22365c
--- /dev/null
+++ b/src/test/java/com/ard333/springbootwebfluxjjwt/rest/ResourceAccessTest.java
@@ -0,0 +1,98 @@
+package com.ard333.springbootwebfluxjjwt.rest;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import org.junit.jupiter.api.Test;
+
+import com.ard333.springbootwebfluxjjwt.model.Message;
+import com.ard333.springbootwebfluxjjwt.model.security.AuthResponse;
+import com.ard333.springbootwebfluxjjwt.utils.BaseRestTest;
+
+public class ResourceAccessTest extends BaseRestTest {
+ private static String REQ_MAPPING = "/resource";
+ private static String USER_URL = REQ_MAPPING + "/user";
+ private static String ADMIN_URL = REQ_MAPPING + "/admin";
+ private static String USER_OR_ADMIN_URL = REQ_MAPPING + "/user-or-admin";
+
+ @Test
+ public void whenInvalidToken_shouldRespondWith401() {
+
+ webClient()
+ .get()
+ .uri(USER_URL)
+ .exchangeToMono(res -> {
+ assertTrue(res.statusCode().is4xxClientError());
+ return res.bodyToMono(AuthResponse.class);
+ })
+ .doOnError(e -> fail("error on test", e))
+ .block();
+ }
+
+ @Test
+ public void whenTokenValid_and_userHasAccess_responseShouldBe2xx() {
+
+ userWebClient()
+ .transform(m -> m.flatMap(c -> c.get()
+ .uri(USER_URL)
+ .exchangeToMono(res -> {
+ assertTrue(res.statusCode().is2xxSuccessful());
+ return res.bodyToMono(Message.class);
+ })))
+ .doOnError(e -> fail("error on test", e))
+ .block();
+ }
+
+ @Test
+ public void whenTokenValid_and_doNotHasAccess_responseShouldBe4xx() {
+
+ userWebClient()
+ .transform(m -> m.flatMap(c -> c.get()
+ .uri(ADMIN_URL)
+ .exchangeToMono(res -> {
+ assertTrue(res.statusCode().is4xxClientError());
+ return res.bodyToMono(Message.class);
+ })))
+ .doOnError(e -> fail("error on test", e))
+ .block();
+ }
+
+ @Test
+ public void whenTokenValid_and_adminHasAccess_responseShouldBe2xx() {
+
+ adminWebClient()
+ .transform(m -> m.flatMap(c -> c.get()
+ .uri(ADMIN_URL)
+ .exchangeToMono(res -> {
+ assertTrue(res.statusCode().is2xxSuccessful());
+ return res.bodyToMono(Message.class);
+ })))
+ .doOnError(e -> fail("error on test", e))
+ .block();
+ }
+
+ @Test
+ public void whenTokenValid_and_adminAndUserHasAccess_responseShouldBe2xx() {
+
+ adminWebClient()
+ .transform(m -> m.flatMap(c -> c.get()
+ .uri(USER_OR_ADMIN_URL)
+ .exchangeToMono(res -> {
+ assertTrue(res.statusCode().is2xxSuccessful());
+ return res.bodyToMono(Message.class);
+ })))
+ .doOnError(e -> fail("error on test", e))
+ .block();
+
+ userWebClient()
+ .transform(m -> m.flatMap(c -> c.get()
+ .uri(USER_OR_ADMIN_URL)
+ .exchangeToMono(res -> {
+ assertTrue(res.statusCode().is2xxSuccessful());
+ return res.bodyToMono(Message.class);
+ })))
+ .doOnError(e -> fail("error on test", e))
+ .block();
+ }
+
+}
diff --git a/src/test/java/com/ard333/springbootwebfluxjjwt/utils/BaseRestTest.java b/src/test/java/com/ard333/springbootwebfluxjjwt/utils/BaseRestTest.java
new file mode 100644
index 0000000..96ca853
--- /dev/null
+++ b/src/test/java/com/ard333/springbootwebfluxjjwt/utils/BaseRestTest.java
@@ -0,0 +1,143 @@
+package com.ard333.springbootwebfluxjjwt.utils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.jupiter.api.Assertions;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+import org.springframework.boot.web.client.RestTemplateBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.reactive.function.client.WebClient;
+
+import com.ard333.springbootwebfluxjjwt.model.security.AuthResponse;
+import com.ard333.springbootwebfluxjjwt.security.model.AuthRequest;
+
+import reactor.core.publisher.Mono;
+
+@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
+@Configuration
+public abstract class BaseRestTest {
+
+ protected static String LOGIN_URL = "/login";
+
+ protected Map tokenMap = new HashMap<>();
+
+ @Value(value = "${local.server.port}")
+ protected int port;
+
+ @Autowired
+ protected TestRestTemplate restTemplate;
+
+ @Autowired
+ protected TestRestTemplate userRestTemplate;
+
+ @Bean
+ public TestRestTemplate userRestTemplate() {
+ RestTemplateBuilder builder = new RestTemplateBuilder(rt -> rt.getInterceptors().add((request, body, execution) -> {
+ request.getHeaders().add("Authorization", new StringBuilder("Bearer ").append(getUserToken())
+ .toString());
+ return execution.execute(request, body);
+ }));
+ return new TestRestTemplate(builder);
+ }
+
+ public StringBuilder createURL(String resource) {
+ return new StringBuilder("http://localhost:")
+ .append(port)
+ .append(resource);
+ }
+
+ protected ResponseEntity userLogin() {
+ AuthRequest req = AuthRequest.builder()
+ .username("user")
+ .password("user")
+ .build();
+
+ ResponseEntity res = restTemplate.postForEntity(
+ createURL(LOGIN_URL).toString(),
+ req,
+ AuthResponse.class);
+ return res;
+ }
+
+ @SuppressWarnings("null")
+ protected String getUserToken() {
+ ResponseEntity userLogin = userLogin();
+ String token = "";
+ try {
+ token = userLogin.getBody().getToken();
+ } catch (Exception e) {
+ }
+ return token;
+ }
+
+ protected HttpEntity