From 696768d6b070cc3169826421c7b2d4a081a5ec7c Mon Sep 17 00:00:00 2001 From: mperor Date: Thu, 20 Feb 2025 12:03:29 +0100 Subject: [PATCH] Configure security with symmetric JWT key & separate existing implementation --- spring-security-lab/README.md | 3 +- spring-security-lab/pom.xml | 7 ++- .../mperor/lab/spring/AppConfiguration.java | 34 +++++++++++ .../mperor/lab/spring/api/ApiController.java | 15 +++++ .../mperor/lab/spring/api/AuthController.java | 22 +++++++ .../mperor/lab/spring/api/TokenService.java | 41 +++++++++++++ .../spring/api/config/ApiSecurityConfig.java | 58 +++++++++++++++++++ .../{ => greetings}/GreetingService.java | 2 +- .../{ => greetings}/GreetingsController.java | 3 +- .../config/GreetingsSecurityConfig.java} | 32 ++-------- .../config/HackerAuthenticationProvider.java | 4 +- .../config/RobotAuthentication.java | 4 +- .../config/RobotAuthenticationProvider.java | 2 +- .../{ => greetings}/config/RobotFilter.java | 4 +- .../config/RobotLoginConfigurer.java | 2 +- .../src/main/resources/application.yml | 5 +- .../lab/spring/api/ApiControllerTest.java | 53 +++++++++++++++++ .../lab/spring/api/AuthControllerTest.java | 33 +++++++++++ .../spring/api/JwtSecretKeyGeneratorTest.java | 38 ++++++++++++ 19 files changed, 319 insertions(+), 43 deletions(-) create mode 100644 spring-security-lab/src/main/java/pl/mperor/lab/spring/AppConfiguration.java create mode 100644 spring-security-lab/src/main/java/pl/mperor/lab/spring/api/ApiController.java create mode 100644 spring-security-lab/src/main/java/pl/mperor/lab/spring/api/AuthController.java create mode 100644 spring-security-lab/src/main/java/pl/mperor/lab/spring/api/TokenService.java create mode 100644 spring-security-lab/src/main/java/pl/mperor/lab/spring/api/config/ApiSecurityConfig.java rename spring-security-lab/src/main/java/pl/mperor/lab/spring/{ => greetings}/GreetingService.java (91%) rename spring-security-lab/src/main/java/pl/mperor/lab/spring/{ => greetings}/GreetingsController.java (93%) rename spring-security-lab/src/main/java/pl/mperor/lab/spring/{config/SecurityConfig.java => greetings/config/GreetingsSecurityConfig.java} (51%) rename spring-security-lab/src/main/java/pl/mperor/lab/spring/{ => greetings}/config/HackerAuthenticationProvider.java (89%) rename spring-security-lab/src/main/java/pl/mperor/lab/spring/{ => greetings}/config/RobotAuthentication.java (95%) rename spring-security-lab/src/main/java/pl/mperor/lab/spring/{ => greetings}/config/RobotAuthenticationProvider.java (95%) rename spring-security-lab/src/main/java/pl/mperor/lab/spring/{ => greetings}/config/RobotFilter.java (98%) rename spring-security-lab/src/main/java/pl/mperor/lab/spring/{ => greetings}/config/RobotLoginConfigurer.java (97%) create mode 100644 spring-security-lab/src/test/java/pl/mperor/lab/spring/api/ApiControllerTest.java create mode 100644 spring-security-lab/src/test/java/pl/mperor/lab/spring/api/AuthControllerTest.java create mode 100644 spring-security-lab/src/test/java/pl/mperor/lab/spring/api/JwtSecretKeyGeneratorTest.java 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 + } +}