Skip to content

Commit c2874e2

Browse files
fix : make @PreAuthorize throwing exceptions clear and add it to test codes
1 parent f542bcb commit c2874e2

File tree

11 files changed

+132
-95
lines changed

11 files changed

+132
-95
lines changed

README.md

+23-16
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<dependency>
77
<groupId>io.github.patternknife.securityhelper.oauth2.api</groupId>
88
<artifactId>spring-security-oauth2-password-jpa-implementation</artifactId>
9-
<version>2.6.0</version>
9+
<version>2.7.0</version>
1010
</dependency>
1111
```
1212
* 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 @@
4444

4545
## Dependencies
4646

47-
| Category | Dependencies |
48-
|-------------------|--------------------------------------------|
49-
| Backend-Language | Java 17 |
50-
| Backend-Framework | Spring Boot 3.1.2 |
51-
| Main Libraries | Spring Security Authorization Server 1.2.3 |
52-
| Package-Manager | Maven 3.6.3 (mvnw, Dockerfile) |
53-
| RDBMS | Mysql 8.0.17 |
47+
| Category | Dependencies |
48+
|-------------------|-------------------------------------------------------------------|
49+
| Backend-Language | Java 17 |
50+
| Backend-Framework | Spring Boot 3.1.2 |
51+
| Main Libraries | Spring Security 6.1.2, Spring Security Authorization Server 1.2.3 |
52+
| Package-Manager | Maven 3.6.3 (mvnw, Dockerfile) |
53+
| RDBMS | Mysql 8.0.17 |
5454

5555
## Run the App
5656

@@ -119,27 +119,34 @@ public class CommonDataSourceConfiguration {
119119
### **Implementation of...**
120120

121121
#### "Mandatory" settings
122+
122123
- The only mandatory setting is ``client.config.securityimpl.service.userdetail.CustomUserDetailsServiceFactory``. The rest depend on your specific situation.
123124

124125
#### "Customizable" settings
125126

126-
- **Use PointCut when events happen such as tokens created**
127+
- **Insert your code when events happen such as tokens created**
127128
- ``SecurityPointCut``
128-
- See the source code in ``client.config.securityimpl.aop``
129+
- See the source code in ``client.config.securityimpl.aop``
130+
131+
129132
- **Register error user messages as desired**
130133
- ``ISecurityUserExceptionMessageService``
131134
- See the source code in ``client.config.securityimpl.message``
135+
136+
132137
- **Customize the whole error payload as desired for all cases**
133138
- What is "all cases"?
134139
- Authorization Server ("/oauth2/token", "/api/v1/traditional-oauth/token") and Resource Server (Bearer token inspection : 401, Permission : 403)
135-
- Customize two points such as
136-
- ``client.config.securityimpl.response.CustomAuthenticationFailureHandlerImpl`` ("/oauth2/token")
137-
- ``client.config.response.error.GlobalExceptionHandler`` ("/api/v1/traditional-oauth/token", Resource Server (Bearer token inspection))
138-
- ``client.config.securityimpl.response.CustomAuthenticationEntryPointImpl`` (Resource Server (Bearer token inspection : 401))
139-
- ``client.config.securityimpl.response.CustomAccessDeniedHandlerImpl`` (Resource Server (Permission inspection : 403))
140+
- Customize errors of the following cases
141+
- Login (/oauth2/token) : ``client.config.securityimpl.response.CustomAuthenticationFailureHandlerImpl``
142+
- Login (/api/v1/traditional-oauth/token) : ``client.config.response.error.GlobalExceptionHandler.authenticationException`` ("/api/v1/traditional-oauth/token", Resource Server (Bearer token inspection))
143+
- Resource Server (Bearer token expired or with a wrong value, 401) :``client.config.securityimpl.response.CustomAuthenticationEntryPointImpl``
144+
- Resource Server (Permission, 403, @PreAuthorized on your APIs) ``client.config.response.error.GlobalExceptionHandler.authorizationException``
145+
146+
140147
- **Customize the whole success payload as desired for the only "/oauth2/token"**
141148
- ``client.config.securityimpl.response.CustomAuthenticationSuccessHandlerImpl``
142-
- The success response payload of "/api/v1/traditional-oauth/token" is in ``api.domain.traditionaloauth.dto``, which doesn't yet to be customizable.
149+
- The success response payload of "/api/v1/traditional-oauth/token" is in ``api.domain.traditionaloauth.dto`` and is not yet customizable.
143150

144151
## Running this App with Docker
145152
* Use the following module for Blue-Green deployment:

client/pom.xml

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ http://maven.apache.org/xsd/maven-4.0.0.xsd">
77
<modelVersion>4.0.0</modelVersion>
88
<groupId>com.patternknife.securityhelper.oauth2.client</groupId>
99
<artifactId>spring-security-oauth2-password-jpa-implementation-client</artifactId>
10-
<version>2.6.0</version>
10+
<version>2.7.0</version>
1111
<packaging>jar</packaging>
1212

1313
<properties>
@@ -41,7 +41,7 @@ http://maven.apache.org/xsd/maven-4.0.0.xsd">
4141
<dependency>
4242
<groupId>io.github.patternknife.securityhelper.oauth2.api</groupId>
4343
<artifactId>spring-security-oauth2-password-jpa-implementation</artifactId>
44-
<version>2.6.0</version>
44+
<version>2.7.0</version>
4545
</dependency>
4646

4747
<!-- DB -->

client/src/main/java/com/patternknife/securityhelper/oauth2/client/config/response/error/GlobalExceptionHandler.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
* Customize the exception payload by implementing this, which replaces
3030
* 'io.github.patternknife.securityhelper.oauth2.api.config.security.response.error.handler.SecurityKnifeExceptionHandler'
3131
*
32-
* 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'.
33-
*
32+
* 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'.
33+
* "OrderConstants.SECURITY_KNIFE_EXCEPTION_HANDLER_ORDER - 1" means this is prior to "SecurityKnifeExceptionHandler"
3434
* */
3535
@Order(OrderConstants.SECURITY_KNIFE_EXCEPTION_HANDLER_ORDER - 1)
3636
@ControllerAdvice

client/src/main/java/com/patternknife/securityhelper/oauth2/client/config/securityimpl/response/CustomAccessDeniedHandlerImpl.java

-27
This file was deleted.

client/src/main/java/com/patternknife/securityhelper/oauth2/client/domain/customer/api/CustomerApi.java

+7-3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import com.patternknife.securityhelper.oauth2.client.domain.customer.dto.CustomerResDTO;
1010
import com.patternknife.securityhelper.oauth2.client.domain.customer.service.CustomerService;
1111
import com.patternknife.securityhelper.oauth2.client.util.CustomUtils;
12+
import jakarta.annotation.Nullable;
1213
import jakarta.servlet.http.HttpServletRequest;
1314
import jakarta.validation.Valid;
1415
import lombok.AllArgsConstructor;
@@ -40,8 +41,6 @@ public class CustomerApi {
4041
@GetMapping("/customers/me")
4142
public CustomerResDTO.IdNameWithAccessTokenRemainingSeconds getCustomerSelf(@AuthenticationPrincipal AccessTokenUserInfo accessTokenUserInfo,
4243
@RequestHeader("Authorization") String authorizationHeader) throws ResourceNotFoundException {
43-
44-
4544
String token = authorizationHeader.substring("Bearer ".length());
4645

4746
int accessTokenRemainingSeconds = 0;
@@ -102,7 +101,12 @@ public Map<String, Boolean> logoutCustomer(HttpServletRequest request) {
102101
return response;
103102
}
104103

105-
104+
@PreAuthorize("@resourceServerAuthorityChecker.hasRole('CUSTOMER_ADMIN')")
105+
@GetMapping("/customers/{id}")
106+
public @Nullable CustomerResDTO.Id getCustomerForAuthorizationTest(@PathVariable final long id)
107+
throws ResourceNotFoundException {
108+
return null;
109+
}
106110

107111
@PreAuthorize("@resourceServerAuthorityChecker.hasRole('CUSTOMER_ADMIN')")
108112
@PutMapping("/customers/{id}")

client/src/main/java/com/patternknife/securityhelper/oauth2/client/domain/customer/service/CustomerService.java

-9
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,4 @@ public CustomerResDTO.Id update(Long id, CustomerReqDTO.Update dto) {
9797
}
9898

9999

100-
public boolean checkIdNameDuplicate(String idName) {
101-
return customerRepository.existsByIdName(idName);
102-
}
103-
104-
public boolean checkHpDuplicate(String hp) {
105-
return customerRepository.existsByHp(CustomUtils.removeSpecialCharacters(hp));
106-
}
107-
108-
109100
}

client/src/test/java/com/patternknife/securityhelper/oauth2/client/integration/auth/CustomerIntegrationTest.java

+92
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import org.junit.jupiter.api.BeforeEach;
1111
import org.junit.jupiter.api.Test;
1212
import org.junit.jupiter.api.extension.ExtendWith;
13+
import org.slf4j.Logger;
14+
import org.slf4j.LoggerFactory;
1315
import org.springframework.beans.factory.annotation.Autowired;
1416
import org.springframework.beans.factory.annotation.Value;
1517
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
@@ -62,6 +64,9 @@
6264
@AutoConfigureRestDocs(outputDir = "target/generated-snippets",uriScheme = "https", uriHost = "vholic.com", uriPort = 8300)
6365
public class CustomerIntegrationTest {
6466

67+
private static final Logger logger = LoggerFactory.getLogger(CustomerIntegrationTest.class);
68+
69+
6570
@Autowired
6671
private MockMvc mockMvc;
6772

@@ -625,6 +630,93 @@ public void testLoginWithInvalidCredentials_EXPOSE() throws Exception {
625630
assertEquals(userMessage, CustomSecurityUserExceptionMessage.AUTHENTICATION_WRONG_GRANT_TYPE.getMessage());
626631
}
627632

633+
@Test
634+
public void testFetchResourceWithInvalidCredentialsAndValidCredentialsButWithNoPermission() throws Exception {
635+
636+
MvcResult result = mockMvc.perform(RestDocumentationRequestBuilders.post("/oauth2/token")
637+
.header(HttpHeaders.AUTHORIZATION, basicHeader)
638+
.header(KnifeHttpHeaders.APP_TOKEN, "APPTOKENTESTRESOURCE")
639+
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
640+
.param("grant_type", "password")
641+
.param("username", testUserName)
642+
.param("password", testUserPassword))
643+
.andExpect(status().isOk())
644+
.andDo(document( "{class-name}/{method-name}/oauth-access-token",
645+
preprocessRequest(new AccessTokenMaskingPreprocessor()),
646+
preprocessResponse(new AccessTokenMaskingPreprocessor(), prettyPrint()),
647+
requestHeaders(
648+
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)"),
649+
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.")
650+
),
651+
formParameters(
652+
parameterWithName("grant_type").description("Uses the password method among Oauth2 grant_types. Please write password."),
653+
parameterWithName("username").description("This is the user's email address."),
654+
parameterWithName("password").description("This is the user's password.")
655+
)))
656+
.andReturn();
657+
658+
659+
String responseString = result.getResponse().getContentAsString(StandardCharsets.UTF_8);
660+
JSONObject jsonResponse = new JSONObject(responseString);
661+
String finalAccessTokenForTestResource = jsonResponse.getString("access_token");
662+
663+
664+
665+
result = mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/customers/5")
666+
.contentType(MediaType.APPLICATION_JSON)
667+
.header(HttpHeaders.AUTHORIZATION, "Bearer " + finalAccessTokenForTestResource + "1"))
668+
.andDo(document( "{class-name}/{method-name}/customers-id",
669+
preprocessRequest(new AccessTokenMaskingPreprocessor()),
670+
preprocessResponse(new AccessTokenMaskingPreprocessor(), prettyPrint()),
671+
requestHeaders(
672+
headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer XXX")
673+
)))
674+
.andExpect(status().isUnauthorized()).andReturn(); // 401
675+
676+
responseString = result.getResponse().getContentAsString(StandardCharsets.UTF_8);
677+
jsonResponse = new JSONObject(responseString);
678+
679+
680+
String userMessage = jsonResponse.getString("userMessage");
681+
682+
assertEquals(userMessage, CustomSecurityUserExceptionMessage.AUTHENTICATION_TOKEN_FAILURE.getMessage());
683+
684+
685+
686+
687+
result = mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/customers/5")
688+
.contentType(MediaType.APPLICATION_JSON)
689+
.header(HttpHeaders.AUTHORIZATION, "Bearer " + finalAccessTokenForTestResource))
690+
.andDo(document( "{class-name}/{method-name}/customers-id",
691+
preprocessRequest(new AccessTokenMaskingPreprocessor()),
692+
preprocessResponse(new AccessTokenMaskingPreprocessor(), prettyPrint()),
693+
requestHeaders(
694+
headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer XXX")
695+
)))
696+
.andExpect(status().isForbidden()).andReturn(); // 403
697+
698+
responseString = result.getResponse().getContentAsString(StandardCharsets.UTF_8);
699+
jsonResponse = new JSONObject(responseString);
700+
userMessage = jsonResponse.getString("userMessage");
701+
702+
assertEquals(userMessage, CustomSecurityUserExceptionMessage.AUTHORIZATION_FAILURE.getMessage());
703+
704+
705+
// Remove Access Token DB done tested
706+
mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/customers/me/logout")
707+
.contentType(MediaType.APPLICATION_JSON)
708+
.header(HttpHeaders.AUTHORIZATION, "Bearer " + finalAccessTokenForTestResource))
709+
710+
.andDo(document( "{class-name}/{method-name}/oauth-customer-logout",
711+
requestHeaders(
712+
headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer XXX")
713+
),relaxedResponseFields(
714+
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.")
715+
716+
)));
717+
}
718+
719+
628720

629721

630722
private static class AccessTokenMaskingPreprocessor implements OperationPreprocessor {

pom.xml

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ http://maven.apache.org/xsd/maven-4.0.0.xsd">
88

99
<groupId>io.github.patternknife.securityhelper.oauth2.api</groupId>
1010
<artifactId>spring-security-oauth2-password-jpa-implementation</artifactId>
11-
<version>2.6.0</version>
11+
<version>2.7.0</version>
1212
<name>spring-security-oauth2-password-jpa-implementation</name>
1313
<description>The implementation of Spring Security 6 Spring Authorization Server for stateful OAuth2 Password Grant</description>
1414
<packaging>jar</packaging>
@@ -340,7 +340,7 @@ http://maven.apache.org/xsd/maven-4.0.0.xsd">
340340
</execution>
341341
</executions>
342342
</plugin>
343-
<!-- <plugin>
343+
<!-- <plugin>
344344
<groupId>org.apache.maven.plugins</groupId>
345345
<artifactId>maven-gpg-plugin</artifactId>
346346
<version>3.0.1</version>

src/main/java/io/github/patternknife/securityhelper/oauth2/api/config/security/response/error/handler/SecurityKnifeExceptionHandler.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public ResponseEntity<?> authenticationException(Exception ex, WebRequest reques
4545
return new ResponseEntity<>(errorResponsePayload, HttpStatus.UNAUTHORIZED);
4646
}
4747

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

src/main/java/io/github/patternknife/securityhelper/oauth2/api/config/security/response/resource/authentication/DefaultAccessDeniedHandler.java

-23
This file was deleted.

0 commit comments

Comments
 (0)