From d056b4d6e2aefeb1c5911ea3ddb679dc4bd81819 Mon Sep 17 00:00:00 2001 From: Phong <58473133+phongphongg@users.noreply.github.com> Date: Sun, 30 Nov 2025 13:04:59 +0700 Subject: [PATCH] Add authentication security tests --- modules/authentication/build.gradle.kts | 6 ++ .../filter/JsonLoginAuthenticationFilter.java | 3 +- .../controller/AuthControllerTest.java | 62 +++++++++++++ .../controller/package-info.java | 4 + .../JsonLoginAuthenticationFilterTest.java | 90 +++++++++++++++++++ .../infrastructure/filter/package-info.java | 4 + .../handler/JsonAccessDeniedHandlerTest.java | 56 ++++++++++++ .../JsonAuthenticationEntryPointTest.java | 56 ++++++++++++ .../infrastructure/handler/package-info.java | 4 + 9 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 modules/authentication/src/test/java/com/workastra/authentication/controller/AuthControllerTest.java create mode 100644 modules/authentication/src/test/java/com/workastra/authentication/controller/package-info.java create mode 100644 modules/authentication/src/test/java/com/workastra/authentication/infrastructure/filter/JsonLoginAuthenticationFilterTest.java create mode 100644 modules/authentication/src/test/java/com/workastra/authentication/infrastructure/filter/package-info.java create mode 100644 modules/authentication/src/test/java/com/workastra/authentication/infrastructure/handler/JsonAccessDeniedHandlerTest.java create mode 100644 modules/authentication/src/test/java/com/workastra/authentication/infrastructure/handler/JsonAuthenticationEntryPointTest.java create mode 100644 modules/authentication/src/test/java/com/workastra/authentication/infrastructure/handler/package-info.java diff --git a/modules/authentication/build.gradle.kts b/modules/authentication/build.gradle.kts index 8d0e47f..1b61c1f 100644 --- a/modules/authentication/build.gradle.kts +++ b/modules/authentication/build.gradle.kts @@ -8,6 +8,12 @@ dependencies { implementation(project(":modules:common")) implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-security-oauth2-client") + testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.boot:spring-boot-starter-security-oauth2-client-test") testImplementation("org.springframework.boot:spring-boot-starter-security-test") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +tasks.withType { + useJUnitPlatform() } diff --git a/modules/authentication/src/main/java/com/workastra/authentication/infrastructure/filter/JsonLoginAuthenticationFilter.java b/modules/authentication/src/main/java/com/workastra/authentication/infrastructure/filter/JsonLoginAuthenticationFilter.java index 55a8043..da6c1b2 100644 --- a/modules/authentication/src/main/java/com/workastra/authentication/infrastructure/filter/JsonLoginAuthenticationFilter.java +++ b/modules/authentication/src/main/java/com/workastra/authentication/infrastructure/filter/JsonLoginAuthenticationFilter.java @@ -11,6 +11,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import tools.jackson.core.JacksonException; import tools.jackson.databind.ObjectMapper; /** @@ -54,7 +55,7 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ ); setDetails(request, authentication); return getAuthenticationManager().authenticate(authentication); - } catch (IOException e) { + } catch (JacksonException | IOException e) { throw new BadCredentialsException("Invalid login request", e); } } diff --git a/modules/authentication/src/test/java/com/workastra/authentication/controller/AuthControllerTest.java b/modules/authentication/src/test/java/com/workastra/authentication/controller/AuthControllerTest.java new file mode 100644 index 0000000..f10784d --- /dev/null +++ b/modules/authentication/src/test/java/com/workastra/authentication/controller/AuthControllerTest.java @@ -0,0 +1,62 @@ +package com.workastra.authentication.controller; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.workastra.common.api.ApiResponse; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.csrf.DefaultCsrfToken; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +class AuthControllerTest { + + private final AuthController controller = new AuthController(); + private MockHttpServletRequest request; + + @BeforeEach + void setUpRequestContext() { + request = new MockHttpServletRequest(); + request.addHeader("X-Request-Id", "test-request"); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + } + + @AfterEach + void clearRequestContext() { + RequestContextHolder.resetRequestAttributes(); + } + + @Test + void getCsrfWrapsTokenInApiResponse() { + CsrfToken csrfToken = new DefaultCsrfToken("X-CSRF", "_csrf", "token-123"); + + ApiResponse response = controller.getCsrf(csrfToken); + + assertTrue(response.success()); + assertEquals("OK", response.code()); + assertNull(response.message()); + assertSame(csrfToken, response.data()); + assertNull(response.errors()); + assertNotNull(response.meta()); + } + + @Test + void meReturnsAuthenticatedPrincipal() { + Authentication authentication = new UsernamePasswordAuthenticationToken("user", "password"); + + ApiResponse response = controller.me(authentication); + + assertTrue(response.success()); + assertEquals("OK", response.code()); + assertSame(authentication, response.data()); + } +} diff --git a/modules/authentication/src/test/java/com/workastra/authentication/controller/package-info.java b/modules/authentication/src/test/java/com/workastra/authentication/controller/package-info.java new file mode 100644 index 0000000..fbafabf --- /dev/null +++ b/modules/authentication/src/test/java/com/workastra/authentication/controller/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package com.workastra.authentication.controller; + +import org.jspecify.annotations.NullMarked; diff --git a/modules/authentication/src/test/java/com/workastra/authentication/infrastructure/filter/JsonLoginAuthenticationFilterTest.java b/modules/authentication/src/test/java/com/workastra/authentication/infrastructure/filter/JsonLoginAuthenticationFilterTest.java new file mode 100644 index 0000000..a69b684 --- /dev/null +++ b/modules/authentication/src/test/java/com/workastra/authentication/infrastructure/filter/JsonLoginAuthenticationFilterTest.java @@ -0,0 +1,90 @@ +package com.workastra.authentication.infrastructure.filter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import jakarta.servlet.http.HttpServletResponse; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpMethod; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.util.Assert; +import org.mockito.Mockito; +import tools.jackson.databind.ObjectMapper; + +class JsonLoginAuthenticationFilterTest { + + private AuthenticationManager authenticationManager; + + private JsonLoginAuthenticationFilter filter; + + @BeforeEach + void setUp() { + authenticationManager = Mockito.mock(AuthenticationManager.class); + Assert.notNull(authenticationManager, "authenticationManager"); + filter = new JsonLoginAuthenticationFilter(authenticationManager, new ObjectMapper()); + } + + @Test + void attemptAuthenticationDelegatesToManagerWhenJsonPayloadIsValid() { + Authentication authenticated = new TestingAuthenticationToken("alice", "password"); + when(authenticationManager.authenticate(any(Authentication.class))).thenReturn(authenticated); + + MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.POST.name(), "/api/v1/auth/login"); + request.setContent("{\"username\":\"alice\",\"password\":\"secret\"}".getBytes(StandardCharsets.UTF_8)); + MockHttpServletResponse response = new MockHttpServletResponse(); + + Authentication result = filter.attemptAuthentication(request, response); + + verify(authenticationManager).authenticate( + argThat(auth -> { + assertTrue(auth instanceof UsernamePasswordAuthenticationToken); + assertEquals("alice", auth.getPrincipal()); + assertEquals("secret", auth.getCredentials()); + return true; + }) + ); + assertEquals(authenticated, result); + } + + @Test + void attemptAuthenticationRejectsNonPostMethods() { + MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.GET.name(), "/api/v1/auth/login"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + AuthenticationServiceException exception = assertThrows( + AuthenticationServiceException.class, + () -> filter.attemptAuthentication(request, response) + ); + + assertEquals("Authentication method not supported: GET", exception.getMessage()); + } + + @Test + void attemptAuthenticationRejectsInvalidJson() { + MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.POST.name(), "/api/v1/auth/login"); + request.setContent("not-a-json".getBytes(StandardCharsets.UTF_8)); + MockHttpServletResponse response = new MockHttpServletResponse(); + + BadCredentialsException exception = assertThrows( + BadCredentialsException.class, + () -> filter.attemptAuthentication(request, response) + ); + + assertEquals("Invalid login request", exception.getMessage()); + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + } +} diff --git a/modules/authentication/src/test/java/com/workastra/authentication/infrastructure/filter/package-info.java b/modules/authentication/src/test/java/com/workastra/authentication/infrastructure/filter/package-info.java new file mode 100644 index 0000000..2b05b0c --- /dev/null +++ b/modules/authentication/src/test/java/com/workastra/authentication/infrastructure/filter/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package com.workastra.authentication.infrastructure.filter; + +import org.jspecify.annotations.NullMarked; diff --git a/modules/authentication/src/test/java/com/workastra/authentication/infrastructure/handler/JsonAccessDeniedHandlerTest.java b/modules/authentication/src/test/java/com/workastra/authentication/infrastructure/handler/JsonAccessDeniedHandlerTest.java new file mode 100644 index 0000000..38c6ed9 --- /dev/null +++ b/modules/authentication/src/test/java/com/workastra/authentication/infrastructure/handler/JsonAccessDeniedHandlerTest.java @@ -0,0 +1,56 @@ +package com.workastra.authentication.infrastructure.handler; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import jakarta.servlet.http.HttpServletResponse; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; + +class JsonAccessDeniedHandlerTest { + + private static final TypeReference> MAP_TYPE = new TypeReference<>() {}; + + @AfterEach + void tearDown() { + RequestContextHolder.resetRequestAttributes(); + } + + @Test + void handleWritesForbiddenApiResponse() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("X-Request-Id", "request-456"); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + + MockHttpServletResponse response = new MockHttpServletResponse(); + JsonAccessDeniedHandler handler = new JsonAccessDeniedHandler(new ObjectMapper()); + + handler.handle(request, response, new AccessDeniedException("denied")); + + assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus()); + assertEquals("application/json", response.getContentType()); + + Map body = new ObjectMapper().readValue(response.getContentAsString(), MAP_TYPE); + Object success = body.get("success"); + assertTrue(success instanceof Boolean); + assertFalse(((Boolean) success).booleanValue()); + assertEquals("FORBIDDEN", body.get("code")); + assertEquals("You do not have permission to access this resource.", body.get("message")); + + Object metaObj = body.get("meta"); + assertTrue(metaObj instanceof Map); + Map meta = (Map) metaObj; + assertEquals("request-456", meta.get("requestId")); + assertNotNull(meta.get("timestamp")); + } +} diff --git a/modules/authentication/src/test/java/com/workastra/authentication/infrastructure/handler/JsonAuthenticationEntryPointTest.java b/modules/authentication/src/test/java/com/workastra/authentication/infrastructure/handler/JsonAuthenticationEntryPointTest.java new file mode 100644 index 0000000..5d3429c --- /dev/null +++ b/modules/authentication/src/test/java/com/workastra/authentication/infrastructure/handler/JsonAuthenticationEntryPointTest.java @@ -0,0 +1,56 @@ +package com.workastra.authentication.infrastructure.handler; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import jakarta.servlet.http.HttpServletResponse; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; + +class JsonAuthenticationEntryPointTest { + + private static final TypeReference> MAP_TYPE = new TypeReference<>() {}; + + @AfterEach + void tearDown() { + RequestContextHolder.resetRequestAttributes(); + } + + @Test + void commenceWritesUnauthorizedApiResponse() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("X-Request-Id", "request-123"); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + + MockHttpServletResponse response = new MockHttpServletResponse(); + JsonAuthenticationEntryPoint entryPoint = new JsonAuthenticationEntryPoint(new ObjectMapper()); + + entryPoint.commence(request, response, new BadCredentialsException("bad")); + + assertEquals(HttpServletResponse.SC_UNAUTHORIZED, response.getStatus()); + assertEquals("application/json", response.getContentType()); + + Map body = new ObjectMapper().readValue(response.getContentAsString(), MAP_TYPE); + Object success = body.get("success"); + assertTrue(success instanceof Boolean); + assertFalse(((Boolean) success).booleanValue()); + assertEquals("UNAUTHORIZED", body.get("code")); + assertEquals("Authentication is required to access this resource.", body.get("message")); + + Object metaObj = body.get("meta"); + assertTrue(metaObj instanceof Map); + Map meta = (Map) metaObj; + assertEquals("request-123", meta.get("requestId")); + assertNotNull(meta.get("timestamp")); + } +} diff --git a/modules/authentication/src/test/java/com/workastra/authentication/infrastructure/handler/package-info.java b/modules/authentication/src/test/java/com/workastra/authentication/infrastructure/handler/package-info.java new file mode 100644 index 0000000..366b952 --- /dev/null +++ b/modules/authentication/src/test/java/com/workastra/authentication/infrastructure/handler/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package com.workastra.authentication.infrastructure.handler; + +import org.jspecify.annotations.NullMarked;