diff --git a/README.md b/README.md index 6387a59..ad04502 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ io.github.patternknife.securityhelper.oauth2.api spring-security-oauth2-password-jpa-implementation - 2.6.0 + 2.7.0 ``` * Set up the same access & refresh token APIs on both ``/oauth2/token`` and on our controller layer such as ``/api/v1/traditional-oauth/token``, both of which function same and have `the same request & response payloads for success and errors`. (However, ``/oauth2/token`` is the standard that "spring-authorization-server" provides.) @@ -44,13 +44,13 @@ ## Dependencies -| Category | Dependencies | -|-------------------|--------------------------------------------| -| Backend-Language | Java 17 | -| Backend-Framework | Spring Boot 3.1.2 | -| Main Libraries | Spring Security Authorization Server 1.2.3 | -| Package-Manager | Maven 3.6.3 (mvnw, Dockerfile) | -| RDBMS | Mysql 8.0.17 | +| Category | Dependencies | +|-------------------|-------------------------------------------------------------------| +| Backend-Language | Java 17 | +| Backend-Framework | Spring Boot 3.1.2 | +| Main Libraries | Spring Security 6.1.2, Spring Security Authorization Server 1.2.3 | +| Package-Manager | Maven 3.6.3 (mvnw, Dockerfile) | +| RDBMS | Mysql 8.0.17 | ## Run the App @@ -119,27 +119,34 @@ public class CommonDataSourceConfiguration { ### **Implementation of...** #### "Mandatory" settings + - The only mandatory setting is ``client.config.securityimpl.service.userdetail.CustomUserDetailsServiceFactory``. The rest depend on your specific situation. #### "Customizable" settings - - **Use PointCut when events happen such as tokens created** + - **Insert your code when events happen such as tokens created** - ``SecurityPointCut`` - - See the source code in ``client.config.securityimpl.aop`` + - See the source code in ``client.config.securityimpl.aop`` + + - **Register error user messages as desired** - ``ISecurityUserExceptionMessageService`` - See the source code in ``client.config.securityimpl.message`` + + - **Customize the whole error payload as desired for all cases** - What is "all cases"? - Authorization Server ("/oauth2/token", "/api/v1/traditional-oauth/token") and Resource Server (Bearer token inspection : 401, Permission : 403) - - Customize two points such as - - ``client.config.securityimpl.response.CustomAuthenticationFailureHandlerImpl`` ("/oauth2/token") - - ``client.config.response.error.GlobalExceptionHandler`` ("/api/v1/traditional-oauth/token", Resource Server (Bearer token inspection)) - - ``client.config.securityimpl.response.CustomAuthenticationEntryPointImpl`` (Resource Server (Bearer token inspection : 401)) - - ``client.config.securityimpl.response.CustomAccessDeniedHandlerImpl`` (Resource Server (Permission inspection : 403)) + - Customize errors of the following cases + - Login (/oauth2/token) : ``client.config.securityimpl.response.CustomAuthenticationFailureHandlerImpl`` + - Login (/api/v1/traditional-oauth/token) : ``client.config.response.error.GlobalExceptionHandler.authenticationException`` ("/api/v1/traditional-oauth/token", Resource Server (Bearer token inspection)) + - Resource Server (Bearer token expired or with a wrong value, 401) :``client.config.securityimpl.response.CustomAuthenticationEntryPointImpl`` + - Resource Server (Permission, 403, @PreAuthorized on your APIs) ``client.config.response.error.GlobalExceptionHandler.authorizationException`` + + - **Customize the whole success payload as desired for the only "/oauth2/token"** - ``client.config.securityimpl.response.CustomAuthenticationSuccessHandlerImpl`` - - The success response payload of "/api/v1/traditional-oauth/token" is in ``api.domain.traditionaloauth.dto``, which doesn't yet to be customizable. + - The success response payload of "/api/v1/traditional-oauth/token" is in ``api.domain.traditionaloauth.dto`` and is not yet customizable. ## Running this App with Docker * Use the following module for Blue-Green deployment: diff --git a/client/pom.xml b/client/pom.xml index 4edc622..ba04a61 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -7,7 +7,7 @@ http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 com.patternknife.securityhelper.oauth2.client spring-security-oauth2-password-jpa-implementation-client - 2.6.0 + 2.7.0 jar @@ -41,7 +41,7 @@ http://maven.apache.org/xsd/maven-4.0.0.xsd"> io.github.patternknife.securityhelper.oauth2.api spring-security-oauth2-password-jpa-implementation - 2.6.0 + 2.7.0 diff --git a/client/src/main/java/com/patternknife/securityhelper/oauth2/client/config/response/error/GlobalExceptionHandler.java b/client/src/main/java/com/patternknife/securityhelper/oauth2/client/config/response/error/GlobalExceptionHandler.java index a032a2f..ede2ed6 100644 --- a/client/src/main/java/com/patternknife/securityhelper/oauth2/client/config/response/error/GlobalExceptionHandler.java +++ b/client/src/main/java/com/patternknife/securityhelper/oauth2/client/config/response/error/GlobalExceptionHandler.java @@ -29,8 +29,8 @@ * Customize the exception payload by implementing this, which replaces * 'io.github.patternknife.securityhelper.oauth2.api.config.security.response.error.handler.SecurityKnifeExceptionHandler' * - * Once you create 'GlobalExceptionHandler', you should insert the following two as default. Otherwise, 'unhandledExceptionHandler' is prior to 'io.github.patternknife.securityhelper.oauth2.api.config.security.response.error.handler.SecurityKnifeExceptionHandler'. - * + * Once you create 'GlobalExceptionHandler', you should insert the following two (authenticationException, authorizationException) as default. Otherwise, 'unhandledExceptionHandler' is prior to 'io.github.patternknife.securityhelper.oauth2.api.config.security.response.error.handler.SecurityKnifeExceptionHandler'. + * "OrderConstants.SECURITY_KNIFE_EXCEPTION_HANDLER_ORDER - 1" means this is prior to "SecurityKnifeExceptionHandler" * */ @Order(OrderConstants.SECURITY_KNIFE_EXCEPTION_HANDLER_ORDER - 1) @ControllerAdvice diff --git a/client/src/main/java/com/patternknife/securityhelper/oauth2/client/config/securityimpl/response/CustomAccessDeniedHandlerImpl.java b/client/src/main/java/com/patternknife/securityhelper/oauth2/client/config/securityimpl/response/CustomAccessDeniedHandlerImpl.java deleted file mode 100644 index 7012368..0000000 --- a/client/src/main/java/com/patternknife/securityhelper/oauth2/client/config/securityimpl/response/CustomAccessDeniedHandlerImpl.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.patternknife.securityhelper.oauth2.client.config.securityimpl.response; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.web.access.AccessDeniedHandler; -import org.springframework.web.servlet.HandlerExceptionResolver; - -import java.io.IOException; - -@Configuration -@RequiredArgsConstructor -public class CustomAccessDeniedHandlerImpl implements AccessDeniedHandler { - - @Qualifier("handlerExceptionResolver") - private final HandlerExceptionResolver resolver; - - @Override - public void handle(HttpServletRequest request, HttpServletResponse response, - AccessDeniedException accessDeniedException) throws IOException { - - resolver.resolveException(request, response, null, accessDeniedException); - } -} diff --git a/client/src/main/java/com/patternknife/securityhelper/oauth2/client/domain/customer/api/CustomerApi.java b/client/src/main/java/com/patternknife/securityhelper/oauth2/client/domain/customer/api/CustomerApi.java index 58ca38c..440b7dd 100644 --- a/client/src/main/java/com/patternknife/securityhelper/oauth2/client/domain/customer/api/CustomerApi.java +++ b/client/src/main/java/com/patternknife/securityhelper/oauth2/client/domain/customer/api/CustomerApi.java @@ -9,6 +9,7 @@ import com.patternknife.securityhelper.oauth2.client.domain.customer.dto.CustomerResDTO; import com.patternknife.securityhelper.oauth2.client.domain.customer.service.CustomerService; import com.patternknife.securityhelper.oauth2.client.util.CustomUtils; +import jakarta.annotation.Nullable; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.AllArgsConstructor; @@ -40,8 +41,6 @@ public class CustomerApi { @GetMapping("/customers/me") public CustomerResDTO.IdNameWithAccessTokenRemainingSeconds getCustomerSelf(@AuthenticationPrincipal AccessTokenUserInfo accessTokenUserInfo, @RequestHeader("Authorization") String authorizationHeader) throws ResourceNotFoundException { - - String token = authorizationHeader.substring("Bearer ".length()); int accessTokenRemainingSeconds = 0; @@ -102,7 +101,12 @@ public Map logoutCustomer(HttpServletRequest request) { return response; } - + @PreAuthorize("@resourceServerAuthorityChecker.hasRole('CUSTOMER_ADMIN')") + @GetMapping("/customers/{id}") + public @Nullable CustomerResDTO.Id getCustomerForAuthorizationTest(@PathVariable final long id) + throws ResourceNotFoundException { + return null; + } @PreAuthorize("@resourceServerAuthorityChecker.hasRole('CUSTOMER_ADMIN')") @PutMapping("/customers/{id}") diff --git a/client/src/main/java/com/patternknife/securityhelper/oauth2/client/domain/customer/service/CustomerService.java b/client/src/main/java/com/patternknife/securityhelper/oauth2/client/domain/customer/service/CustomerService.java index 1b637e6..fc6cbb4 100644 --- a/client/src/main/java/com/patternknife/securityhelper/oauth2/client/domain/customer/service/CustomerService.java +++ b/client/src/main/java/com/patternknife/securityhelper/oauth2/client/domain/customer/service/CustomerService.java @@ -97,13 +97,4 @@ public CustomerResDTO.Id update(Long id, CustomerReqDTO.Update dto) { } - public boolean checkIdNameDuplicate(String idName) { - return customerRepository.existsByIdName(idName); - } - - public boolean checkHpDuplicate(String hp) { - return customerRepository.existsByHp(CustomUtils.removeSpecialCharacters(hp)); - } - - } \ No newline at end of file diff --git a/client/src/test/java/com/patternknife/securityhelper/oauth2/client/integration/auth/CustomerIntegrationTest.java b/client/src/test/java/com/patternknife/securityhelper/oauth2/client/integration/auth/CustomerIntegrationTest.java index 26b77a2..418fb87 100644 --- a/client/src/test/java/com/patternknife/securityhelper/oauth2/client/integration/auth/CustomerIntegrationTest.java +++ b/client/src/test/java/com/patternknife/securityhelper/oauth2/client/integration/auth/CustomerIntegrationTest.java @@ -10,6 +10,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; @@ -62,6 +64,9 @@ @AutoConfigureRestDocs(outputDir = "target/generated-snippets",uriScheme = "https", uriHost = "vholic.com", uriPort = 8300) public class CustomerIntegrationTest { + private static final Logger logger = LoggerFactory.getLogger(CustomerIntegrationTest.class); + + @Autowired private MockMvc mockMvc; @@ -625,6 +630,93 @@ public void testLoginWithInvalidCredentials_EXPOSE() throws Exception { assertEquals(userMessage, CustomSecurityUserExceptionMessage.AUTHENTICATION_WRONG_GRANT_TYPE.getMessage()); } + @Test + public void testFetchResourceWithInvalidCredentialsAndValidCredentialsButWithNoPermission() throws Exception { + + MvcResult result = mockMvc.perform(RestDocumentationRequestBuilders.post("/oauth2/token") + .header(HttpHeaders.AUTHORIZATION, basicHeader) + .header(KnifeHttpHeaders.APP_TOKEN, "APPTOKENTESTRESOURCE") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("grant_type", "password") + .param("username", testUserName) + .param("password", testUserPassword)) + .andExpect(status().isOk()) + .andDo(document( "{class-name}/{method-name}/oauth-access-token", + preprocessRequest(new AccessTokenMaskingPreprocessor()), + preprocessResponse(new AccessTokenMaskingPreprocessor(), prettyPrint()), + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("Connect the received client_id and client_secret with ':', use the base64 function, and write Basic at the beginning. ex) Basic base64(client_id:client_secret)"), + headerWithName(KnifeHttpHeaders.APP_TOKEN).optional().description("Not having a value does not mean you cannot log in, but cases without an App-Token value share the same access_token. Please include it as a required value according to the device-specific session policy.") + ), + formParameters( + parameterWithName("grant_type").description("Uses the password method among Oauth2 grant_types. Please write password."), + parameterWithName("username").description("This is the user's email address."), + parameterWithName("password").description("This is the user's password.") + ))) + .andReturn(); + + + String responseString = result.getResponse().getContentAsString(StandardCharsets.UTF_8); + JSONObject jsonResponse = new JSONObject(responseString); + String finalAccessTokenForTestResource = jsonResponse.getString("access_token"); + + + + result = mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/customers/5") + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + finalAccessTokenForTestResource + "1")) + .andDo(document( "{class-name}/{method-name}/customers-id", + preprocessRequest(new AccessTokenMaskingPreprocessor()), + preprocessResponse(new AccessTokenMaskingPreprocessor(), prettyPrint()), + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer XXX") + ))) + .andExpect(status().isUnauthorized()).andReturn(); // 401 + + responseString = result.getResponse().getContentAsString(StandardCharsets.UTF_8); + jsonResponse = new JSONObject(responseString); + + + String userMessage = jsonResponse.getString("userMessage"); + + assertEquals(userMessage, CustomSecurityUserExceptionMessage.AUTHENTICATION_TOKEN_FAILURE.getMessage()); + + + + + result = mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/customers/5") + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + finalAccessTokenForTestResource)) + .andDo(document( "{class-name}/{method-name}/customers-id", + preprocessRequest(new AccessTokenMaskingPreprocessor()), + preprocessResponse(new AccessTokenMaskingPreprocessor(), prettyPrint()), + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer XXX") + ))) + .andExpect(status().isForbidden()).andReturn(); // 403 + + responseString = result.getResponse().getContentAsString(StandardCharsets.UTF_8); + jsonResponse = new JSONObject(responseString); + userMessage = jsonResponse.getString("userMessage"); + + assertEquals(userMessage, CustomSecurityUserExceptionMessage.AUTHORIZATION_FAILURE.getMessage()); + + + // Remove Access Token DB done tested + mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/customers/me/logout") + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + finalAccessTokenForTestResource)) + + .andDo(document( "{class-name}/{method-name}/oauth-customer-logout", + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer XXX") + ),relaxedResponseFields( + fieldWithPath("logout").description("If true, logout is successful on the backend, if false, it fails. However, ignore this message and, considering UX, delete the token on the client side and move to the login screen.") + + ))); + } + + private static class AccessTokenMaskingPreprocessor implements OperationPreprocessor { diff --git a/pom.xml b/pom.xml index ca53819..9795954 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ http://maven.apache.org/xsd/maven-4.0.0.xsd"> io.github.patternknife.securityhelper.oauth2.api spring-security-oauth2-password-jpa-implementation - 2.6.0 + 2.7.0 spring-security-oauth2-password-jpa-implementation The implementation of Spring Security 6 Spring Authorization Server for stateful OAuth2 Password Grant jar @@ -340,7 +340,7 @@ http://maven.apache.org/xsd/maven-4.0.0.xsd"> -