Skip to content

Commit

Permalink
fix : make @PreAuthorize throwing exceptions clear and add it to test…
Browse files Browse the repository at this point in the history
… codes
  • Loading branch information
patternknife committed Jul 18, 2024
1 parent f542bcb commit c2874e2
Show file tree
Hide file tree
Showing 11 changed files with 132 additions and 95 deletions.
39 changes: 23 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<dependency>
<groupId>io.github.patternknife.securityhelper.oauth2.api</groupId>
<artifactId>spring-security-oauth2-password-jpa-implementation</artifactId>
<version>2.6.0</version>
<version>2.7.0</version>
</dependency>
```
* 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.)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions client/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.patternknife.securityhelper.oauth2.client</groupId>
<artifactId>spring-security-oauth2-password-jpa-implementation-client</artifactId>
<version>2.6.0</version>
<version>2.7.0</version>
<packaging>jar</packaging>

<properties>
Expand Down Expand Up @@ -41,7 +41,7 @@ http://maven.apache.org/xsd/maven-4.0.0.xsd">
<dependency>
<groupId>io.github.patternknife.securityhelper.oauth2.api</groupId>
<artifactId>spring-security-oauth2-password-jpa-implementation</artifactId>
<version>2.6.0</version>
<version>2.7.0</version>
</dependency>

<!-- DB -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -102,7 +101,12 @@ public Map<String, Boolean> 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}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ http://maven.apache.org/xsd/maven-4.0.0.xsd">

<groupId>io.github.patternknife.securityhelper.oauth2.api</groupId>
<artifactId>spring-security-oauth2-password-jpa-implementation</artifactId>
<version>2.6.0</version>
<version>2.7.0</version>
<name>spring-security-oauth2-password-jpa-implementation</name>
<description>The implementation of Spring Security 6 Spring Authorization Server for stateful OAuth2 Password Grant</description>
<packaging>jar</packaging>
Expand Down Expand Up @@ -340,7 +340,7 @@ http://maven.apache.org/xsd/maven-4.0.0.xsd">
</execution>
</executions>
</plugin>
<!-- <plugin>
<!-- <plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>3.0.1</version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public ResponseEntity<?> authenticationException(Exception ex, WebRequest reques
return new ResponseEntity<>(errorResponsePayload, HttpStatus.UNAUTHORIZED);
}

// 403 : Authorization
// 403 : Authorization (= Forbidden, AccessDenied)
@ExceptionHandler({ AccessDeniedException.class })
public ResponseEntity<?> authorizationException(Exception ex, WebRequest request) {
ErrorResponsePayload errorResponsePayload = new ErrorResponsePayload(ex.getMessage() != null ? ex.getMessage() : ExceptionKnifeUtils.getAllCauses(ex), request.getDescription(false),
Expand Down

This file was deleted.

Loading

0 comments on commit c2874e2

Please sign in to comment.