diff --git a/spring-security-lab/README.md b/spring-security-lab/README.md
index 02f9650..31544b5 100644
--- a/spring-security-lab/README.md
+++ b/spring-security-lab/README.md
@@ -3,4 +3,5 @@
## Sources π
* [Spring Security, demystified by Daniel Garnier Moiroux 2022](https://youtu.be/iJ2muJniikY)
-* [Spring Security Architecture Principles by Daniel Garnier-Moiroux 2024](https://youtu.be/HyoLl3VcRFY?si=DmpYBQCvsztM7Ubi)
\ No newline at end of file
+* [Spring Security Architecture Principles by Daniel Garnier-Moiroux 2024](https://youtu.be/HyoLl3VcRFY?si=DmpYBQCvsztM7Ubi)
+* [How to Secure your REST APIs with Spring Security & JSON Web Tokens (JWTs)](https://www.danvega.dev/blog/spring-security-jwt)
\ No newline at end of file
diff --git a/spring-security-lab/pom.xml b/spring-security-lab/pom.xml
index 5d03efe..1b5f4d2 100644
--- a/spring-security-lab/pom.xml
+++ b/spring-security-lab/pom.xml
@@ -22,9 +22,14 @@
org.springframework.boot
spring-boot-starter-web
+
org.springframework.boot
- spring-boot-starter-security
+ spring-boot-starter-oauth2-resource-server
+
+
+ org.springframework.security
+ spring-security-test
diff --git a/spring-security-lab/src/main/java/pl/mperor/lab/spring/AppConfiguration.java b/spring-security-lab/src/main/java/pl/mperor/lab/spring/AppConfiguration.java
new file mode 100644
index 0000000..150a471
--- /dev/null
+++ b/spring-security-lab/src/main/java/pl/mperor/lab/spring/AppConfiguration.java
@@ -0,0 +1,34 @@
+package pl.mperor.lab.spring;
+
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.provisioning.InMemoryUserDetailsManager;
+
+@Configuration
+public class AppConfiguration {
+
+ @Bean
+ public UserDetailsService userDetailsService() {
+ return new InMemoryUserDetailsManager(
+ User.builder()
+ .username("user")
+ .password("{noop}password")
+ .authorities("READ","ROLE_USER")
+ .build()
+ );
+ }
+
+ @Bean
+ public ApplicationListener successListener() {
+ return event -> System.out.printf(
+ "π SUCCESS [%s] πβ%s πβ%s%n",
+ event.getAuthentication().getClass().getSimpleName(),
+ event.getAuthentication().getName(),
+ event.getAuthentication().getAuthorities()
+ );
+ }
+}
diff --git a/spring-security-lab/src/main/java/pl/mperor/lab/spring/api/ApiController.java b/spring-security-lab/src/main/java/pl/mperor/lab/spring/api/ApiController.java
new file mode 100644
index 0000000..4d1b2eb
--- /dev/null
+++ b/spring-security-lab/src/main/java/pl/mperor/lab/spring/api/ApiController.java
@@ -0,0 +1,15 @@
+package pl.mperor.lab.spring.api;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api")
+public class ApiController {
+
+ @GetMapping("/hello")
+ String hello() {
+ return "Hello World π";
+ }
+}
diff --git a/spring-security-lab/src/main/java/pl/mperor/lab/spring/api/AuthController.java b/spring-security-lab/src/main/java/pl/mperor/lab/spring/api/AuthController.java
new file mode 100644
index 0000000..035b0a0
--- /dev/null
+++ b/spring-security-lab/src/main/java/pl/mperor/lab/spring/api/AuthController.java
@@ -0,0 +1,22 @@
+package pl.mperor.lab.spring.api;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/auth")
+public class AuthController {
+
+ private final TokenService tokenService;
+
+ public AuthController(TokenService tokenService) {
+ this.tokenService = tokenService;
+ }
+
+ @PostMapping("/token")
+ public String token(Authentication authentication) {
+ return tokenService.generateToken(authentication);
+ }
+}
\ No newline at end of file
diff --git a/spring-security-lab/src/main/java/pl/mperor/lab/spring/api/TokenService.java b/spring-security-lab/src/main/java/pl/mperor/lab/spring/api/TokenService.java
new file mode 100644
index 0000000..063c550
--- /dev/null
+++ b/spring-security-lab/src/main/java/pl/mperor/lab/spring/api/TokenService.java
@@ -0,0 +1,41 @@
+package pl.mperor.lab.spring.api;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
+import org.springframework.security.oauth2.jwt.JwsHeader;
+import org.springframework.security.oauth2.jwt.JwtClaimsSet;
+import org.springframework.security.oauth2.jwt.JwtEncoder;
+import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
+import org.springframework.stereotype.Service;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.stream.Collectors;
+
+@Service
+public class TokenService {
+
+ private final JwtEncoder encoder;
+
+ public TokenService(JwtEncoder encoder) {
+ this.encoder = encoder;
+ }
+
+ public String generateToken(Authentication authentication) {
+ Instant now = Instant.now();
+ String scope = authentication.getAuthorities().stream()
+ .map(GrantedAuthority::getAuthority)
+ .filter(authority -> !authority.startsWith("ROLE"))
+ .collect(Collectors.joining(" "));
+ JwtClaimsSet claims = JwtClaimsSet.builder()
+ .issuer("self")
+ .issuedAt(now)
+ .expiresAt(now.plus(1, ChronoUnit.HOURS))
+ .subject(authentication.getName())
+ .claim("scope", scope)
+ .build();
+ var encoderParameters = JwtEncoderParameters.from(JwsHeader.with(MacAlgorithm.HS512).build(), claims);
+ return this.encoder.encode(encoderParameters).getTokenValue();
+ }
+}
\ No newline at end of file
diff --git a/spring-security-lab/src/main/java/pl/mperor/lab/spring/api/config/ApiSecurityConfig.java b/spring-security-lab/src/main/java/pl/mperor/lab/spring/api/config/ApiSecurityConfig.java
new file mode 100644
index 0000000..8235611
--- /dev/null
+++ b/spring-security-lab/src/main/java/pl/mperor/lab/spring/api/config/ApiSecurityConfig.java
@@ -0,0 +1,58 @@
+package pl.mperor.lab.spring.api.config;
+
+import com.nimbusds.jose.jwk.source.ImmutableSecret;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.oauth2.jwt.JwtEncoder;
+import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
+import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+
+import javax.crypto.spec.SecretKeySpec;
+
+import static org.springframework.security.config.Customizer.withDefaults;
+
+@Configuration
+@EnableWebSecurity
+public class ApiSecurityConfig {
+
+ private String jwtKey;
+
+ ApiSecurityConfig(@Value("${jwt.key}") String jwtKey) {
+ this.jwtKey = jwtKey;
+ }
+
+ @Bean
+ public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
+ return http
+ .securityMatcher("/api/**")
+ .csrf(AbstractHttpConfigurer::disable)
+ .authorizeHttpRequests(auth -> auth
+ .requestMatchers("/api/auth/token").hasRole("USER")
+ .anyRequest().hasAuthority("SCOPE_READ")
+ )
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .oauth2ResourceServer(configurer -> configurer.jwt(withDefaults()))
+ .httpBasic(withDefaults())
+ .build();
+ }
+
+ @Bean
+ public JwtEncoder jwtEncoder() {
+ return new NimbusJwtEncoder(new ImmutableSecret<>(jwtKey.getBytes()));
+ }
+
+ @Bean
+ public JwtDecoder jwtDecoder() {
+ byte[] bytes = jwtKey.getBytes();
+ SecretKeySpec originalKey = new SecretKeySpec(bytes, 0, bytes.length, "HmacSHA256");
+ return NimbusJwtDecoder.withSecretKey(originalKey).macAlgorithm(MacAlgorithm.HS512).build();
+ }
+}
diff --git a/spring-security-lab/src/main/java/pl/mperor/lab/spring/GreetingService.java b/spring-security-lab/src/main/java/pl/mperor/lab/spring/greetings/GreetingService.java
similarity index 91%
rename from spring-security-lab/src/main/java/pl/mperor/lab/spring/GreetingService.java
rename to spring-security-lab/src/main/java/pl/mperor/lab/spring/greetings/GreetingService.java
index b43f075..bcf0034 100644
--- a/spring-security-lab/src/main/java/pl/mperor/lab/spring/GreetingService.java
+++ b/spring-security-lab/src/main/java/pl/mperor/lab/spring/greetings/GreetingService.java
@@ -1,4 +1,4 @@
-package pl.mperor.lab.spring;
+package pl.mperor.lab.spring.greetings;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
diff --git a/spring-security-lab/src/main/java/pl/mperor/lab/spring/GreetingsController.java b/spring-security-lab/src/main/java/pl/mperor/lab/spring/greetings/GreetingsController.java
similarity index 93%
rename from spring-security-lab/src/main/java/pl/mperor/lab/spring/GreetingsController.java
rename to spring-security-lab/src/main/java/pl/mperor/lab/spring/greetings/GreetingsController.java
index ab6300c..40fcd08 100644
--- a/spring-security-lab/src/main/java/pl/mperor/lab/spring/GreetingsController.java
+++ b/spring-security-lab/src/main/java/pl/mperor/lab/spring/greetings/GreetingsController.java
@@ -1,4 +1,4 @@
-package pl.mperor.lab.spring;
+package pl.mperor.lab.spring.greetings;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -21,5 +21,4 @@ public String publicPage() {
public String privatePage() {
return "Private page π(Secret room)! " + service.greetUser();
}
-
}
diff --git a/spring-security-lab/src/main/java/pl/mperor/lab/spring/config/SecurityConfig.java b/spring-security-lab/src/main/java/pl/mperor/lab/spring/greetings/config/GreetingsSecurityConfig.java
similarity index 51%
rename from spring-security-lab/src/main/java/pl/mperor/lab/spring/config/SecurityConfig.java
rename to spring-security-lab/src/main/java/pl/mperor/lab/spring/greetings/config/GreetingsSecurityConfig.java
index 23f2a2a..c7bc171 100644
--- a/spring-security-lab/src/main/java/pl/mperor/lab/spring/config/SecurityConfig.java
+++ b/spring-security-lab/src/main/java/pl/mperor/lab/spring/greetings/config/GreetingsSecurityConfig.java
@@ -1,24 +1,20 @@
-package pl.mperor.lab.spring.config;
+package pl.mperor.lab.spring.greetings.config;
-import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
-import org.springframework.security.core.userdetails.User;
-import org.springframework.security.core.userdetails.UserDetailsService;
-import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
-public class SecurityConfig {
+public class GreetingsSecurityConfig {
@Bean
- public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+ public SecurityFilterChain greetingsSecurityFilterChain(HttpSecurity http) throws Exception {
return http
+ .securityMatcher("/**")
.authorizeHttpRequests(authorizeHttp -> {
authorizeHttp.requestMatchers("/").permitAll();
authorizeHttp.requestMatchers("/error").permitAll();
@@ -34,24 +30,4 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.authenticationProvider(new HackerAuthenticationProvider())
.build();
}
-
- @Bean
- public UserDetailsService userDetailsService() {
- return new InMemoryUserDetailsManager(
- User.builder()
- .username("user")
- .password("{noop}password")
- .authorities("ROLE user")
- .build()
- );
- }
-
- @Bean
- public ApplicationListener successListener() {
- return event -> System.out.printf(
- "π SUCCESS [%s] %s%n",
- event.getAuthentication().getClass().getSimpleName(),
- event.getAuthentication().getName()
- );
- }
}
diff --git a/spring-security-lab/src/main/java/pl/mperor/lab/spring/config/HackerAuthenticationProvider.java b/spring-security-lab/src/main/java/pl/mperor/lab/spring/greetings/config/HackerAuthenticationProvider.java
similarity index 89%
rename from spring-security-lab/src/main/java/pl/mperor/lab/spring/config/HackerAuthenticationProvider.java
rename to spring-security-lab/src/main/java/pl/mperor/lab/spring/greetings/config/HackerAuthenticationProvider.java
index e01491f..c34df02 100644
--- a/spring-security-lab/src/main/java/pl/mperor/lab/spring/config/HackerAuthenticationProvider.java
+++ b/spring-security-lab/src/main/java/pl/mperor/lab/spring/greetings/config/HackerAuthenticationProvider.java
@@ -1,4 +1,4 @@
-package pl.mperor.lab.spring.config;
+package pl.mperor.lab.spring.greetings.config;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -15,7 +15,7 @@ public Authentication authenticate(Authentication authentication) throws Authent
return UsernamePasswordAuthenticationToken.authenticated(
"hacker",
null, // π hacker don't need credentials
- AuthorityUtils.createAuthorityList("ROLE_admin")
+ AuthorityUtils.createAuthorityList("ROLE_ADMIN")
);
}
return null;
diff --git a/spring-security-lab/src/main/java/pl/mperor/lab/spring/config/RobotAuthentication.java b/spring-security-lab/src/main/java/pl/mperor/lab/spring/greetings/config/RobotAuthentication.java
similarity index 95%
rename from spring-security-lab/src/main/java/pl/mperor/lab/spring/config/RobotAuthentication.java
rename to spring-security-lab/src/main/java/pl/mperor/lab/spring/greetings/config/RobotAuthentication.java
index d5663fa..16f4312 100644
--- a/spring-security-lab/src/main/java/pl/mperor/lab/spring/config/RobotAuthentication.java
+++ b/spring-security-lab/src/main/java/pl/mperor/lab/spring/greetings/config/RobotAuthentication.java
@@ -1,4 +1,4 @@
-package pl.mperor.lab.spring.config;
+package pl.mperor.lab.spring.greetings.config;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
@@ -25,7 +25,7 @@ public static RobotAuthentication unauthenticated(String password) {
}
public static RobotAuthentication authenticated() {
- return new RobotAuthentication(AuthorityUtils.createAuthorityList("ROLE_robot"), null);
+ return new RobotAuthentication(AuthorityUtils.createAuthorityList("ROLE_ROBOT"), null);
}
@Override
diff --git a/spring-security-lab/src/main/java/pl/mperor/lab/spring/config/RobotAuthenticationProvider.java b/spring-security-lab/src/main/java/pl/mperor/lab/spring/greetings/config/RobotAuthenticationProvider.java
similarity index 95%
rename from spring-security-lab/src/main/java/pl/mperor/lab/spring/config/RobotAuthenticationProvider.java
rename to spring-security-lab/src/main/java/pl/mperor/lab/spring/greetings/config/RobotAuthenticationProvider.java
index 25113b8..b59e2e2 100644
--- a/spring-security-lab/src/main/java/pl/mperor/lab/spring/config/RobotAuthenticationProvider.java
+++ b/spring-security-lab/src/main/java/pl/mperor/lab/spring/greetings/config/RobotAuthenticationProvider.java
@@ -1,4 +1,4 @@
-package pl.mperor.lab.spring.config;
+package pl.mperor.lab.spring.greetings.config;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
diff --git a/spring-security-lab/src/main/java/pl/mperor/lab/spring/config/RobotFilter.java b/spring-security-lab/src/main/java/pl/mperor/lab/spring/greetings/config/RobotFilter.java
similarity index 98%
rename from spring-security-lab/src/main/java/pl/mperor/lab/spring/config/RobotFilter.java
rename to spring-security-lab/src/main/java/pl/mperor/lab/spring/greetings/config/RobotFilter.java
index 1879c52..e91c5bc 100644
--- a/spring-security-lab/src/main/java/pl/mperor/lab/spring/config/RobotFilter.java
+++ b/spring-security-lab/src/main/java/pl/mperor/lab/spring/greetings/config/RobotFilter.java
@@ -1,4 +1,4 @@
-package pl.mperor.lab.spring.config;
+package pl.mperor.lab.spring.greetings.config;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
@@ -60,6 +60,4 @@ protected void doFilterInternal(
// 2. Do the Restβ’οΈ
}
-
-
}
diff --git a/spring-security-lab/src/main/java/pl/mperor/lab/spring/config/RobotLoginConfigurer.java b/spring-security-lab/src/main/java/pl/mperor/lab/spring/greetings/config/RobotLoginConfigurer.java
similarity index 97%
rename from spring-security-lab/src/main/java/pl/mperor/lab/spring/config/RobotLoginConfigurer.java
rename to spring-security-lab/src/main/java/pl/mperor/lab/spring/greetings/config/RobotLoginConfigurer.java
index f3bf60c..7fa2828 100644
--- a/spring-security-lab/src/main/java/pl/mperor/lab/spring/config/RobotLoginConfigurer.java
+++ b/spring-security-lab/src/main/java/pl/mperor/lab/spring/greetings/config/RobotLoginConfigurer.java
@@ -1,4 +1,4 @@
-package pl.mperor.lab.spring.config;
+package pl.mperor.lab.spring.greetings.config;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
diff --git a/spring-security-lab/src/main/resources/application.yml b/spring-security-lab/src/main/resources/application.yml
index 49c0d3b..646d9a7 100644
--- a/spring-security-lab/src/main/resources/application.yml
+++ b/spring-security-lab/src/main/resources/application.yml
@@ -3,4 +3,7 @@
# Useful classes during DEBUG session:
# - FilterChainProxy
-# - DefaultSecurityFilterChain
\ No newline at end of file
+# - DefaultSecurityFilterChain
+
+jwt:
+ key: 404a91cf551282839e3fa9afff8bc366add5c7bf6d983a1c03557ad7c0c050ab
\ No newline at end of file
diff --git a/spring-security-lab/src/test/java/pl/mperor/lab/spring/api/ApiControllerTest.java b/spring-security-lab/src/test/java/pl/mperor/lab/spring/api/ApiControllerTest.java
new file mode 100644
index 0000000..a8a8c6a
--- /dev/null
+++ b/spring-security-lab/src/test/java/pl/mperor/lab/spring/api/ApiControllerTest.java
@@ -0,0 +1,53 @@
+package pl.mperor.lab.spring.api;
+
+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.http.HttpHeaders;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+class ApiControllerTest {
+
+ @Autowired
+ MockMvc mvc;
+
+ @Test
+ void shouldReturnUnauthorizedWithNoJwt() throws Exception {
+ this.mvc.perform(get("/api/hello"))
+ .andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ void shouldReturnUnauthorizedWithInvalidJwt() throws Exception {
+ this.mvc.perform(get("/api/hello").header(HttpHeaders.AUTHORIZATION, "Bearer FAKEINCORRECTTOKEN"))
+ .andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ void shouldReturnWelcomeMessageWithValidJwt() throws Exception {
+ var token = this.mvc.perform(post("/api/auth/token")
+ .with(httpBasic("user", "password")))
+ .andReturn()
+ .getResponse()
+ .getContentAsString();
+
+ assertThat(token).isNotEmpty();
+
+ MvcResult response = this.mvc.perform(get("/api/hello").header(HttpHeaders.AUTHORIZATION, "Bearer " + token))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ assertEquals("Hello World π", response.getResponse().getContentAsString());
+ }
+}
\ No newline at end of file
diff --git a/spring-security-lab/src/test/java/pl/mperor/lab/spring/api/AuthControllerTest.java b/spring-security-lab/src/test/java/pl/mperor/lab/spring/api/AuthControllerTest.java
new file mode 100644
index 0000000..f10d7e5
--- /dev/null
+++ b/spring-security-lab/src/test/java/pl/mperor/lab/spring/api/AuthControllerTest.java
@@ -0,0 +1,33 @@
+package pl.mperor.lab.spring.api;
+
+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.test.web.servlet.MockMvc;
+
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+class AuthControllerTest {
+
+ @Autowired
+ MockMvc mvc;
+
+ @Test
+ void shouldReturnJwtWithValidUserCredentials() throws Exception {
+ this.mvc.perform(post("/api/auth/token")
+ .with(httpBasic("user", "password")))
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ void shouldReturnUnauthorizedWithInValidUserCredentials() throws Exception {
+ this.mvc.perform(post("/api/auth/token")
+ .with(httpBasic("admin", "admin")))
+ .andExpect(status().isUnauthorized());
+ }
+}
\ No newline at end of file
diff --git a/spring-security-lab/src/test/java/pl/mperor/lab/spring/api/JwtSecretKeyGeneratorTest.java b/spring-security-lab/src/test/java/pl/mperor/lab/spring/api/JwtSecretKeyGeneratorTest.java
new file mode 100644
index 0000000..b9fb1d9
--- /dev/null
+++ b/spring-security-lab/src/test/java/pl/mperor/lab/spring/api/JwtSecretKeyGeneratorTest.java
@@ -0,0 +1,38 @@
+package pl.mperor.lab.spring.api;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.security.SecureRandom;
+import java.util.function.Function;
+
+public class JwtSecretKeyGeneratorTest {
+
+ private static final Logger logger = LoggerFactory.getLogger(JwtSecretKeyGeneratorTest.class);
+
+ private Function generator = (bytes) -> {
+ byte[] key = new byte[bytes];
+ new SecureRandom().nextBytes(key);
+ return bytesToHex(key);
+ };
+
+ private static String bytesToHex(byte[] bytes) {
+ StringBuilder hexString = new StringBuilder();
+ for (byte b : bytes) {
+ hexString.append(String.format("%02x", b));
+ }
+ return hexString.toString();
+ }
+
+ @Test
+ public void testGenerateNewJwtSecretKey() {
+ var jwtKey = generator.apply(32); // 256-bit key
+ logger.info("Generated JWT Key: {}", jwtKey);
+ Assertions.assertThat(jwtKey)
+ .isNotNull()
+ .hasSize(64) // 32 bytes in HEX = 64 characters
+ .matches("^[0-9a-fA-F]+$"); // only hexadecimal characters
+ }
+}