Skip to content

Commit f9cb8ab

Browse files
fix : synchronize api/v1/... and oauth2/token for basic token errors
1 parent 2b95df5 commit f9cb8ab

File tree

11 files changed

+246
-184
lines changed

11 files changed

+246
-184
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
```shell
3838
mvnw clean install
3939
cd client
40-
mvnw clean install
40+
mvnw clean install # Integration tests are done here, which creates docs by Spring-Rest-Doc.
4141
```
4242
- Run the client module by running ``SpringSecurityOauth2PasswordJpaImplApplication`` in the client.
4343
- The API information is found on ``http://localhost:8370/docs/api-app.html``, managed by Spring Rest Doc

client/pom.xml

Lines changed: 2 additions & 2 deletions
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.0.0</version>
10+
<version>2.1.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>com.patternknife.securityhelper.oauth2.api</groupId>
4343
<artifactId>spring-security-oauth2-password-jpa-implementation</artifactId>
44-
<version>1.0.0</version>
44+
<version>2.0.0</version>
4545
</dependency>
4646

4747
<!-- DB -->

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

Lines changed: 3 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.patternknife.securityhelper.oauth2.client.config.response.error;
22

33

4+
import com.patternknife.securityhelper.oauth2.api.config.response.error.dto.ErrorResponsePayload;
5+
import com.patternknife.securityhelper.oauth2.api.config.response.error.exception.auth.KnifeOauth2AuthenticationException;
46
import com.patternknife.securityhelper.oauth2.api.config.response.error.message.SecurityUserExceptionMessage;
57
import com.patternknife.securityhelper.oauth2.client.config.response.error.dto.CustomErrorResponsePayload;
68
import com.patternknife.securityhelper.oauth2.client.config.response.error.exception.auth.*;
@@ -35,96 +37,10 @@
3537
import java.util.HashMap;
3638
import java.util.Map;
3739

38-
@Order(Ordered.HIGHEST_PRECEDENCE)
40+
3941
@ControllerAdvice
4042
public class GlobalExceptionHandler {
4143

42-
/*
43-
* General
44-
* */
45-
// Login Failure
46-
@ExceptionHandler({InsufficientAuthenticationException.class, CustomOauth2AuthenticationException.class, AuthenticationException.class})
47-
public ResponseEntity<?> authenticationException(Exception ex, WebRequest request) {
48-
CustomErrorResponsePayload customErrorResponsePayload;
49-
if(ex instanceof CustomOauth2AuthenticationException && ((CustomOauth2AuthenticationException) ex).getErrorMessages() != null) {
50-
customErrorResponsePayload = new CustomErrorResponsePayload(((CustomOauth2AuthenticationException) ex).getErrorMessages(),
51-
ex, request.getDescription(false), CustomExceptionUtils.getAllStackTraces(ex),
52-
CustomExceptionUtils.getAllCauses(ex), null);
53-
}else {
54-
customErrorResponsePayload = new CustomErrorResponsePayload(CustomExceptionUtils.getAllCauses(ex), request.getDescription(false), SecurityUserExceptionMessage.AUTHENTICATION_LOGIN_FAILURE.getMessage(),
55-
ex.getMessage(), ex.getStackTrace()[0].toString());
56-
}
57-
return new ResponseEntity<>(customErrorResponsePayload, HttpStatus.UNAUTHORIZED);
58-
}
59-
// Role (=Permission) Failure
60-
@ExceptionHandler({UnauthorizedException.class, AccessDeniedException.class, DisabledException.class})
61-
public ResponseEntity<?> authorizationException(Exception ex, WebRequest request) {
62-
CustomErrorResponsePayload customErrorResponsePayload = new CustomErrorResponsePayload(ex.getMessage() != null ? ex.getMessage() : CustomExceptionUtils.getAllCauses(ex), request.getDescription(false),
63-
ex.getMessage() == null || ex.getMessage().equals("Access Denied") ? SecurityUserExceptionMessage.AUTHORIZATION_FAILURE.getMessage() : ex.getMessage(), ex.getStackTrace()[0].toString());
64-
return new ResponseEntity<>(customErrorResponsePayload, HttpStatus.FORBIDDEN);
65-
}
66-
// Custom or Admin
67-
@ExceptionHandler({CustomAuthGuardException.class})
68-
public ResponseEntity<?> customAuthorizationException(Exception ex, WebRequest request) {
69-
CustomErrorResponsePayload customErrorResponsePayload = new CustomErrorResponsePayload(CustomExceptionUtils.getAllCauses(ex), request.getDescription(false), SecurityUserExceptionMessage.AUTHORIZATION_FAILURE.getMessage(),
70-
ex.getMessage(), ex.getStackTrace()[0].toString());
71-
return new ResponseEntity<>(customErrorResponsePayload, HttpStatus.FORBIDDEN);
72-
}
73-
74-
/*
75-
* Issues with ID, Password
76-
* */
77-
@ExceptionHandler({UsernameNotFoundException.class, BadCredentialsException.class})
78-
public ResponseEntity<?> usernameOrPasswordIssueException(Exception ex, WebRequest request) {
79-
80-
CustomErrorResponsePayload customErrorResponsePayload = new CustomErrorResponsePayload(CustomExceptionUtils.getAllCauses(ex), request.getDescription(false), ex.getMessage(),
81-
ex.getMessage(), ex.getStackTrace()[0].toString());
82-
83-
return new ResponseEntity<>(customErrorResponsePayload, HttpStatus.UNAUTHORIZED);
84-
}
85-
86-
/*
87-
* Social Login (Access Token Failure)
88-
* */
89-
// 1. NoSocialRegisteredException: Trying to do social login but the user does not exist (TO DO. Need separation. The app is branching based on the message of this Exception)
90-
// 2. AlreadySocialRegisteredException: Trying to create a social user but it already exists
91-
@ExceptionHandler({ AlreadySocialRegisteredException.class, NoSocialRegisteredException.class })
92-
public ResponseEntity<?> socialLoginFailureException(Exception ex, WebRequest request) {
93-
CustomErrorResponsePayload customErrorResponsePayload = new CustomErrorResponsePayload(ex.getMessage(), request.getDescription(false), ex.getMessage(),
94-
CustomExceptionUtils.getAllStackTraces(ex), CustomExceptionUtils.getAllCauses(ex));
95-
return new ResponseEntity<>(customErrorResponsePayload, HttpStatus.UNAUTHORIZED);
96-
}
97-
// SocialEmailNotProvidedException: The app received a 200 status from the social platform using the access token store, but the social platform did not provide the user's email information. In this case, the company needs to obtain authorization from the social platform.
98-
@ExceptionHandler({ SocialEmailNotProvidedException.class})
99-
public ResponseEntity<?> accessToSocialSuccessButIssuesWithReturnedValue(Exception ex, WebRequest request) {
100-
CustomErrorResponsePayload customErrorResponsePayload = new CustomErrorResponsePayload(ex.getMessage(), request.getDescription(false), ex.getMessage(),
101-
CustomExceptionUtils.getAllStackTraces(ex), CustomExceptionUtils.getAllCauses(ex));
102-
return new ResponseEntity<>(customErrorResponsePayload, HttpStatus.FORBIDDEN);
103-
}
104-
105-
// Social Resource Access Failure (Access Token OK but No Permission)
106-
// The social platform has blocked access to the requested resource.
107-
@ExceptionHandler({SocialUnauthorizedException.class})
108-
public ResponseEntity<?> accessToSocialDenied(Exception ex, WebRequest request) {
109-
CustomErrorResponsePayload customErrorResponsePayload = new CustomErrorResponsePayload(ex.getMessage() != null ? ex.getMessage() : CustomExceptionUtils.getAllCauses(ex),
110-
request.getDescription(false),"Not a valid access token." , ex.getStackTrace()[0].toString());
111-
return new ResponseEntity<>(customErrorResponsePayload, HttpStatus.FORBIDDEN);
112-
}
113-
114-
// OTP (Only for Admin)
115-
@ExceptionHandler({OtpValueUnauthorizedException.class})
116-
public ResponseEntity<?> otpException(Exception ex, WebRequest request) {
117-
118-
Map<String, String> userValidationMessages = new HashMap<>();
119-
userValidationMessages.put("otp_value", ex.getMessage());
120-
121-
CustomErrorResponsePayload customErrorResponsePayload = new CustomErrorResponsePayload(ex.getMessage(), request.getDescription(false),
122-
null,
123-
userValidationMessages,
124-
CustomExceptionUtils.getAllStackTraces(ex), CustomExceptionUtils.getAllCauses(ex));
125-
126-
return new ResponseEntity<>(customErrorResponsePayload, HttpStatus.UNAUTHORIZED);
127-
}
12844

12945
// UserDeletedException : caused by the process of user deactivation
13046
// UserRestoredException : caused by the process of user reactivation

client/src/main/java/com/patternknife/securityhelper/oauth2/client/config/securityimpl/serivce/userdetail/CustomerDetailsService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public void setEntityManager(EntityManager entityManager) {
5656
@Override
5757
public UserDetails loadUserByUsername(String username) {
5858

59-
Customer customer = customerRepository.findByIdName(username).orElseThrow(() -> new UsernameNotFoundException("Customer (ID : \"" + username + "\") NOT foUND"));
59+
Customer customer = customerRepository.findByIdName(username).orElseThrow(() -> new UsernameNotFoundException("Customer (ID : \"" + username + "\") NOT Found"));
6060
if(customer.getDeletedAt() != null){
6161
if(customer.getDeleteAdminId() == null) {
6262
if (customer.getOneWeekAfterDeletedAsString() != null) {

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

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.patternknife.securityhelper.oauth2.client.integration.auth;
22

33

4+
import com.patternknife.securityhelper.oauth2.api.config.response.error.message.SecurityUserExceptionMessage;
45
import com.patternknife.securityhelper.oauth2.api.config.security.KnifeHttpHeaders;
56
import jakarta.xml.bind.DatatypeConverter;
67
import lombok.SneakyThrows;
@@ -47,6 +48,13 @@
4748
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
4849
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
4950

51+
52+
53+
/*
54+
* Functions ending with
55+
* "ORIGINAL" : '/oauth2/token'
56+
* "EXPOSED" : '/api/v1/traditional-oauth/token'
57+
* */
5058
@ExtendWith(RestDocumentationExtension.class)
5159
@ExtendWith(SpringExtension.class)
5260
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@@ -432,6 +440,134 @@ public void test_SameAppTokensUseSameAccessToken_ORIGINAL() throws Exception {
432440
}
433441
}
434442

443+
@Test
444+
public void testLoginWithInvalidCredentials_ORIGINAL() throws Exception {
445+
446+
447+
MvcResult result = mockMvc.perform(RestDocumentationRequestBuilders.post("/oauth2/token")
448+
.header(HttpHeaders.AUTHORIZATION, basicHeader)
449+
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
450+
.param("grant_type", "password")
451+
.param("username", testUserName + "wrongcredential")
452+
.param("password", testUserPassword))
453+
.andExpect(status().isUnauthorized()) // 401
454+
.andDo(document( "{class-name}/{method-name}/oauth-access-token",
455+
preprocessRequest(new AccessTokenMaskingPreprocessor()),
456+
preprocessResponse(new AccessTokenMaskingPreprocessor(), prettyPrint()),
457+
requestHeaders(
458+
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)"),
459+
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.")
460+
),
461+
formParameters(
462+
parameterWithName("grant_type").description("Uses the password method among Oauth2 grant_types. Please write password."),
463+
parameterWithName("username").description("This is the user's email address."),
464+
parameterWithName("password").description("This is the user's password.")
465+
)))
466+
.andReturn();
467+
468+
469+
String responseString = result.getResponse().getContentAsString();
470+
JSONObject jsonResponse = new JSONObject(responseString);
471+
String userMessage = jsonResponse.getString("userMessage");
472+
473+
assertEquals(userMessage, SecurityUserExceptionMessage.AUTHENTICATION_LOGIN_FAILURE.getMessage());
474+
475+
476+
477+
result = mockMvc.perform(RestDocumentationRequestBuilders.post("/oauth2/token")
478+
.header(HttpHeaders.AUTHORIZATION, "Basic " + DatatypeConverter.printBase64Binary((appUserClientId + "wrongcred:" + appUserClientSecret).getBytes("UTF-8")))
479+
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
480+
.param("grant_type", "password")
481+
.param("username", testUserName)
482+
.param("password", testUserPassword))
483+
.andExpect(status().isUnauthorized()) // 401
484+
.andDo(document( "{class-name}/{method-name}/oauth-access-token",
485+
preprocessRequest(new AccessTokenMaskingPreprocessor()),
486+
preprocessResponse(new AccessTokenMaskingPreprocessor(), prettyPrint()),
487+
requestHeaders(
488+
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)"),
489+
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.")
490+
),
491+
formParameters(
492+
parameterWithName("grant_type").description("Uses the password method among Oauth2 grant_types. Please write password."),
493+
parameterWithName("username").description("This is the user's email address."),
494+
parameterWithName("password").description("This is the user's password.")
495+
)))
496+
.andReturn();
497+
498+
499+
responseString = result.getResponse().getContentAsString();
500+
jsonResponse = new JSONObject(responseString);
501+
userMessage = jsonResponse.getString("userMessage");
502+
503+
assertEquals(userMessage, SecurityUserExceptionMessage.WRONG_CLIENT_ID_SECRET.getMessage());
504+
}
505+
506+
507+
@Test
508+
public void testLoginWithInvalidCredentials_EXPOSE() throws Exception {
509+
510+
MvcResult result = mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/traditional-oauth/token")
511+
.header(HttpHeaders.AUTHORIZATION, basicHeader)
512+
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
513+
.param("grant_type", "password")
514+
.param("username", testUserName + "wrongcredential")
515+
.param("password", testUserPassword))
516+
.andExpect(status().isUnauthorized()) // 401
517+
.andDo(document( "{class-name}/{method-name}/oauth-access-token",
518+
preprocessRequest(new AccessTokenMaskingPreprocessor()),
519+
preprocessResponse(new AccessTokenMaskingPreprocessor(), prettyPrint()),
520+
requestHeaders(
521+
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)"),
522+
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.")
523+
),
524+
formParameters(
525+
parameterWithName("grant_type").description("Uses the password method among Oauth2 grant_types. Please write password."),
526+
parameterWithName("username").description("This is the user's email address."),
527+
parameterWithName("password").description("This is the user's password.")
528+
)))
529+
.andReturn();
530+
531+
532+
String responseString = result.getResponse().getContentAsString();
533+
JSONObject jsonResponse = new JSONObject(responseString);
534+
String userMessage = jsonResponse.getString("userMessage");
535+
536+
assertEquals(userMessage, SecurityUserExceptionMessage.AUTHENTICATION_LOGIN_FAILURE.getMessage());
537+
538+
539+
540+
result = mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/traditional-oauth/token")
541+
.header(HttpHeaders.AUTHORIZATION, "Basic " + DatatypeConverter.printBase64Binary((appUserClientId + "wrongcred:" + appUserClientSecret).getBytes("UTF-8")))
542+
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
543+
.param("grant_type", "password")
544+
.param("username", testUserName)
545+
.param("password", testUserPassword))
546+
.andExpect(status().isUnauthorized()) // 401
547+
.andDo(document( "{class-name}/{method-name}/oauth-access-token",
548+
preprocessRequest(new AccessTokenMaskingPreprocessor()),
549+
preprocessResponse(new AccessTokenMaskingPreprocessor(), prettyPrint()),
550+
requestHeaders(
551+
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)"),
552+
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.")
553+
),
554+
formParameters(
555+
parameterWithName("grant_type").description("Uses the password method among Oauth2 grant_types. Please write password."),
556+
parameterWithName("username").description("This is the user's email address."),
557+
parameterWithName("password").description("This is the user's password.")
558+
)))
559+
.andReturn();
560+
561+
562+
responseString = result.getResponse().getContentAsString();
563+
jsonResponse = new JSONObject(responseString);
564+
userMessage = jsonResponse.getString("userMessage");
565+
566+
assertEquals(userMessage, SecurityUserExceptionMessage.WRONG_CLIENT_ID_SECRET.getMessage());
567+
}
568+
569+
570+
435571
private static class AccessTokenMaskingPreprocessor implements OperationPreprocessor {
436572

437573
@Override

0 commit comments

Comments
 (0)