Skip to content

Commit fdc5426

Browse files
Feature: Validator
- The Spring Authorization Server recommends using functional interfaces for validation (https://docs.spring.io/spring-authorization-server/reference/protocol-endpoints.html). - Instead of applying OAuth2ClientCredentialsAuthenticationContext, additional parameters from OAuth2ClientAuthenticationToken were used. I could create OAuth2ClientCredentialsAuthenticationContext in OpaqueGrantTypeAuthenticationProvider, but I believe that would be a bit overkill. - Renamed RegisteredClientRepositoryImpl to CacheableRegisteredClientRepositoryImpl to reflect its original caching functionality.
1 parent 5ea02f7 commit fdc5426

File tree

13 files changed

+337
-88
lines changed

13 files changed

+337
-88
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package com.patternhelloworld.securityhelper.oauth2.client.integration.auth;
2+
3+
4+
import jakarta.xml.bind.DatatypeConverter;
5+
import org.junit.jupiter.api.BeforeEach;
6+
import org.junit.jupiter.api.Test;
7+
import org.junit.jupiter.api.extension.ExtendWith;
8+
import org.slf4j.Logger;
9+
import org.slf4j.LoggerFactory;
10+
import org.springframework.beans.factory.annotation.Autowired;
11+
import org.springframework.beans.factory.annotation.Value;
12+
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
13+
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
14+
import org.springframework.boot.test.context.SpringBootTest;
15+
import org.springframework.restdocs.RestDocumentationContextProvider;
16+
import org.springframework.restdocs.RestDocumentationExtension;
17+
import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler;
18+
import org.springframework.test.context.junit.jupiter.SpringExtension;
19+
import org.springframework.test.web.servlet.MockMvc;
20+
import org.springframework.test.web.servlet.MvcResult;
21+
import org.springframework.web.context.WebApplicationContext;
22+
23+
import java.io.UnsupportedEncodingException;
24+
25+
import static org.junit.jupiter.api.Assertions.assertEquals;
26+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
27+
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
28+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
29+
30+
31+
/*
32+
* Functions ending with
33+
* "ORIGINAL" : '/oauth2/token'
34+
* "EXPOSED" : '/api/v1/traditional-oauth/token'
35+
* */
36+
@ExtendWith(RestDocumentationExtension.class)
37+
@ExtendWith(SpringExtension.class)
38+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
39+
@AutoConfigureMockMvc
40+
@AutoConfigureRestDocs(outputDir = "target/generated-snippets",uriScheme = "http", uriHost = "localhost", uriPort = 8370)
41+
public class AuthorizationIntegrationTest {
42+
43+
private static final Logger logger = LoggerFactory.getLogger(AuthorizationIntegrationTest.class);
44+
45+
46+
@Autowired
47+
private MockMvc mockMvc;
48+
49+
50+
@Value("${app.oauth2.appUser.clientId}")
51+
private String appUserClientId;
52+
@Value("${app.oauth2.appUser.clientSecret}")
53+
private String appUserClientSecret;
54+
55+
@Value("${app.test.auth.customer.username}")
56+
private String testUserName;
57+
@Value("${app.test.auth.customer.password}")
58+
private String testUserPassword;
59+
60+
61+
private RestDocumentationResultHandler document;
62+
63+
private String basicHeader;
64+
65+
@Autowired
66+
private WebApplicationContext webApplicationContext;
67+
68+
69+
@BeforeEach
70+
public void setUp(RestDocumentationContextProvider restDocumentationContextProvider) throws UnsupportedEncodingException {
71+
72+
basicHeader = "Basic " + DatatypeConverter.printBase64Binary((appUserClientId + ":" + appUserClientSecret).getBytes("UTF-8"));
73+
74+
}
75+
@Test
76+
public void testAuthorizationCodeMissingException() throws Exception {
77+
MvcResult result = mockMvc.perform(get("/oauth2/authorize?response_type=code&client_id=client_customer&state=xxx&scope=read&redirect_uri=http://localhost:8081/callback1"))
78+
.andExpect(status().is2xxSuccessful())
79+
.andDo(print())
80+
.andReturn();
81+
assertEquals("/login", result.getResponse().getForwardedUrl());
82+
/* String responseContent = result.getResponse().getContentAsString(StandardCharsets.UTF_8);
83+
assertTrue(responseContent.contains("AUTHENTICATION_AUTHORIZATION_CODE_MISSING"),
84+
"The response should contain the error code 'AUTHENTICATION_AUTHORIZATION_CODE_MISSING'.");*/
85+
}
86+
87+
}
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package com.patternhelloworld.securityhelper.oauth2.client.integration.auth;
22

33

4-
import io.github.patternhelloworld.securityhelper.oauth2.api.config.util.EasyPlusHttpHeaders;
54
import com.patternhelloworld.securityhelper.oauth2.client.config.securityimpl.message.CustomSecurityUserExceptionMessage;
5+
import io.github.patternhelloworld.securityhelper.oauth2.api.config.util.EasyPlusHttpHeaders;
66
import jakarta.xml.bind.DatatypeConverter;
77
import lombok.SneakyThrows;
88
import org.codehaus.jackson.map.ObjectMapper;
@@ -61,10 +61,10 @@
6161
@ExtendWith(SpringExtension.class)
6262
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
6363
@AutoConfigureMockMvc
64-
@AutoConfigureRestDocs(outputDir = "target/generated-snippets",uriScheme = "https", uriHost = "vholic.com", uriPort = 8300)
65-
public class CustomerIntegrationTest {
64+
@AutoConfigureRestDocs(outputDir = "target/generated-snippets",uriScheme = "http", uriHost = "localhost", uriPort = 8370)
65+
public class TokenIntegrationTest {
6666

67-
private static final Logger logger = LoggerFactory.getLogger(CustomerIntegrationTest.class);
67+
private static final Logger logger = LoggerFactory.getLogger(TokenIntegrationTest.class);
6868

6969

7070
@Autowired
@@ -808,6 +808,4 @@ public OperationResponse preprocess(OperationResponse response) {
808808
}
809809

810810

811-
812-
813811
}
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@
1212
import java.util.Map;
1313

1414
@RequiredArgsConstructor
15-
public final class OpaqueGrantTypeClientIdMandatoryAccessTokenRequestConverter implements AuthenticationConverter {
15+
public final class TokenRequestAfterClientBasicSecretAuthenticatedConverter implements AuthenticationConverter {
1616

1717
@Override
1818
public Authentication convert(HttpServletRequest request) {
1919

2020
Map<String, Object> allParameters = EasyPlusOAuth2EndpointUtils.getApiParametersContainingEasyPlusHeaders(request);
2121

22-
// ClientId is a must
22+
// The client_id has already been parsed by ClientSecretBasicAuthenticationConverter.
23+
// Therefore, there is no need to validate the client_id again at this point.
2324
String clientId = allParameters.get("client_id").toString();
2425

2526
// All token requests are "CLIENT_SECRET_BASIC"
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package io.github.patternhelloworld.securityhelper.oauth2.api.config.security.provider.auth.endpoint;
16+
package io.github.patternhelloworld.securityhelper.oauth2.api.config.security.provider.auth.endpoint.authorization;
1717

1818
import io.github.patternhelloworld.securityhelper.oauth2.api.config.security.serivce.CommonOAuth2AuthorizationSaver;
1919
import io.github.patternhelloworld.securityhelper.oauth2.api.config.security.serivce.userdetail.ConditionalDetailsService;
@@ -34,8 +34,6 @@
3434

3535
public final class AuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
3636

37-
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
38-
3937
private final OAuth2AuthorizationService authorizationService;
4038

4139
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package io.github.patternhelloworld.securityhelper.oauth2.api.config.security.provider.auth.endpoint;
1+
package io.github.patternhelloworld.securityhelper.oauth2.api.config.security.provider.auth.endpoint.token;
22

33

44
import io.github.patternhelloworld.securityhelper.oauth2.api.config.security.message.DefaultSecurityUserExceptionMessage;
@@ -8,9 +8,11 @@
88
import io.github.patternhelloworld.securityhelper.oauth2.api.config.security.serivce.CommonOAuth2AuthorizationSaver;
99
import io.github.patternhelloworld.securityhelper.oauth2.api.config.security.serivce.DefaultOauth2AuthenticationHashCheckService;
1010
import io.github.patternhelloworld.securityhelper.oauth2.api.config.security.serivce.persistence.authorization.OAuth2AuthorizationServiceImpl;
11-
import io.github.patternhelloworld.securityhelper.oauth2.api.config.security.serivce.persistence.client.RegisteredClientRepositoryImpl;
1211
import io.github.patternhelloworld.securityhelper.oauth2.api.config.security.serivce.userdetail.ConditionalDetailsService;
13-
import lombok.AllArgsConstructor;
12+
import io.github.patternhelloworld.securityhelper.oauth2.api.config.security.validator.endpoint.token.OpaqueGrantTypeTokenValidationResult;
13+
import lombok.RequiredArgsConstructor;
14+
import org.slf4j.Logger;
15+
import org.slf4j.LoggerFactory;
1416
import org.springframework.security.authentication.AuthenticationProvider;
1517
import org.springframework.security.core.Authentication;
1618
import org.springframework.security.core.userdetails.UserDetails;
@@ -25,29 +27,30 @@
2527
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
2628
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
2729
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
28-
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
30+
import org.springframework.util.Assert;
2931

30-
import java.util.Arrays;
3132
import java.util.HashMap;
3233
import java.util.Map;
33-
import java.util.Set;
34-
import java.util.stream.Collectors;
34+
import java.util.function.Function;
3535

3636
/*
3737
* 1) ROPC (grant_type=password, grant_type=refresh_token)
3838
* 2) Authorization Code flow
3939
* - Get an authorization_code with username and password (grant_type=authorization_code)
4040
* - Login with code received from the authorization code flow instead of username & password (grant_type=code)
4141
*/
42-
@AllArgsConstructor
42+
@RequiredArgsConstructor
4343
public final class OpaqueGrantTypeAuthenticationProvider implements AuthenticationProvider {
4444

45+
private static final Logger logger = LoggerFactory.getLogger(OpaqueGrantTypeAuthenticationProvider.class);
46+
47+
private Function<Map<String, Object>, OpaqueGrantTypeTokenValidationResult> authenticationValidator;
48+
4549
private final CommonOAuth2AuthorizationSaver commonOAuth2AuthorizationSaver;
4650
private final ConditionalDetailsService conditionalDetailsService;
4751
private final DefaultOauth2AuthenticationHashCheckService oauth2AuthenticationHashCheckService;
4852
private final OAuth2AuthorizationServiceImpl oAuth2AuthorizationService;
4953
private final ISecurityUserExceptionMessageService iSecurityUserExceptionMessageService;
50-
private final RegisteredClientRepositoryImpl registeredClientRepository;
5154

5255
@Override
5356
public Authentication authenticate(Authentication authentication)
@@ -56,41 +59,26 @@ public Authentication authenticate(Authentication authentication)
5659
try {
5760
if (authentication instanceof OAuth2ClientAuthenticationToken token) {
5861

59-
// [NOTICE] If an incorrect client ID or Secret is detected, the OpaqueGrantTypeAccessTokenRequestConverter is not be invoked, which means there is NO mandatory client_id header parameter.
60-
// For reference, if an incorrect Basic header, such as base64(client_id:<--no secret here-->), is detected, the ClientSecretBasicAuthenticationConverter handles it directly and passes it to the AuthenticationFailureHandler.
61-
String clientId = token.getAdditionalParameters().getOrDefault("client_id", "").toString();
62-
if (clientId.isEmpty()) {
63-
throw new EasyPlusOauth2AuthenticationException(EasyPlusErrorMessages.builder().message("Invalid Request. OpaqueGrantTypeAccessTokenRequestConverter was not invoked. This may indicate incorrect payloads or expired code or code_verifier.").userMessage(iSecurityUserExceptionMessageService.getUserMessage(DefaultSecurityUserExceptionMessage.AUTHENTICATION_LOGIN_ERROR)).build());
64-
}
65-
6662
Map<String, Object> modifiableAdditionalParameters = new HashMap<>(token.getAdditionalParameters());
67-
63+
OpaqueGrantTypeTokenValidationResult opaqueGrantTypeTokenValidationResult = this.authenticationValidator.apply(modifiableAdditionalParameters);
6864

6965
UserDetails userDetails;
70-
71-
String grantType = modifiableAdditionalParameters.getOrDefault("grant_type", "").toString();
72-
if (grantType.isEmpty()) {
73-
throw new EasyPlusOauth2AuthenticationException(EasyPlusErrorMessages.builder().message("No grant_type key found").userMessage(iSecurityUserExceptionMessageService.getUserMessage(DefaultSecurityUserExceptionMessage.AUTHENTICATION_LOGIN_ERROR)).build());
74-
}
75-
76-
String responseType = modifiableAdditionalParameters.getOrDefault("response_type", "").toString();
77-
78-
switch (grantType) {
66+
switch (opaqueGrantTypeTokenValidationResult.getGrantType()) {
7967
case "authorization_code" -> {
80-
OAuth2Authorization oAuth2Authorization = oAuth2AuthorizationService.findByAuthorizationCode((String) modifiableAdditionalParameters.get("code"));
68+
OAuth2Authorization oAuth2Authorization = oAuth2AuthorizationService.findByAuthorizationCode(opaqueGrantTypeTokenValidationResult.getCode());
8169
if (oAuth2Authorization == null) {
82-
throw new EasyPlusOauth2AuthenticationException("authorization code not found");
70+
throw new EasyPlusOauth2AuthenticationException(EasyPlusErrorMessages.builder().message("No user info found for the authorization code").userMessage(iSecurityUserExceptionMessageService.getUserMessage(DefaultSecurityUserExceptionMessage.AUTHENTICATION_LOGIN_FAILURE)).build());
8371
}
84-
userDetails = conditionalDetailsService.loadUserByUsername(oAuth2Authorization.getPrincipalName(), clientId);
72+
userDetails = conditionalDetailsService.loadUserByUsername(oAuth2Authorization.getPrincipalName(), opaqueGrantTypeTokenValidationResult.getClientId());
8573
}
8674
case "password" -> {
87-
userDetails = conditionalDetailsService.loadUserByUsername((String) modifiableAdditionalParameters.get("username"), clientId);
75+
userDetails = conditionalDetailsService.loadUserByUsername((String) modifiableAdditionalParameters.get("username"), opaqueGrantTypeTokenValidationResult.getClientId());
8876
oauth2AuthenticationHashCheckService.validateUsernamePassword((String) modifiableAdditionalParameters.get("password"), userDetails);
8977
}
9078
case "refresh_token" -> {
9179
OAuth2Authorization oAuth2Authorization = oAuth2AuthorizationService.findByToken((String) modifiableAdditionalParameters.get("refresh_token"), OAuth2TokenType.REFRESH_TOKEN);
9280
if (oAuth2Authorization != null) {
93-
userDetails = conditionalDetailsService.loadUserByUsername(oAuth2Authorization.getPrincipalName(), clientId);
81+
userDetails = conditionalDetailsService.loadUserByUsername(oAuth2Authorization.getPrincipalName(), opaqueGrantTypeTokenValidationResult.getClientId());
9482
} else {
9583
throw new EasyPlusOauth2AuthenticationException(iSecurityUserExceptionMessageService.getUserMessage(DefaultSecurityUserExceptionMessage.AUTHENTICATION_LOGIN_ERROR));
9684
}
@@ -99,34 +87,16 @@ public Authentication authenticate(Authentication authentication)
9987
}
10088

10189
// Create tokens at this point
102-
OAuth2Authorization oAuth2Authorization = commonOAuth2AuthorizationSaver.save(userDetails, new AuthorizationGrantType(modifiableAdditionalParameters.get("grant_type").toString()), clientId, modifiableAdditionalParameters);
90+
OAuth2Authorization oAuth2Authorization = commonOAuth2AuthorizationSaver.save(userDetails, new AuthorizationGrantType(modifiableAdditionalParameters.get("grant_type").toString()), opaqueGrantTypeTokenValidationResult.getClientId(), modifiableAdditionalParameters);
10391

104-
RegisteredClient registeredClient = registeredClientRepository.findByClientId(clientId);
105-
if (registeredClient == null) {
106-
throw new EasyPlusOauth2AuthenticationException(EasyPlusErrorMessages.builder().message("client_id NOT found in DB").userMessage(iSecurityUserExceptionMessageService.getUserMessage(DefaultSecurityUserExceptionMessage.AUTHENTICATION_LOGIN_ERROR)).build());
107-
}
108-
Set<String> registeredScopes = registeredClient.getScopes();
109-
Set<String> requestedScopes = Arrays.stream(
110-
modifiableAdditionalParameters.getOrDefault(OAuth2ParameterNames.SCOPE, "")
111-
.toString()
112-
.split(",")
113-
)
114-
.map(String::trim)
115-
.filter(scope -> !scope.isEmpty())
116-
.collect(Collectors.toSet());
117-
if (!registeredScopes.containsAll(requestedScopes)) {
118-
throw new EasyPlusOauth2AuthenticationException(EasyPlusErrorMessages.builder().userMessage(iSecurityUserExceptionMessageService.getUserMessage(DefaultSecurityUserExceptionMessage.AUTHENTICATION_INVALID_REDIRECT_URI))
119-
.message("Invalid scopes: " + requestedScopes + ". Allowed scopes: " + registeredScopes).build());
120-
}
121-
122-
if (responseType.equals(OAuth2ParameterNames.CODE)) {
92+
if (opaqueGrantTypeTokenValidationResult.getResponseType() != null && opaqueGrantTypeTokenValidationResult.getResponseType().equals(OAuth2ParameterNames.CODE)) {
12393
// [IMPORTANT] To return the "code" not "access_token". Check "AuthenticationSuccessHandler".
12494
modifiableAdditionalParameters.put("code", oAuth2Authorization.getToken(OAuth2AuthorizationCode.class).getToken().getTokenValue());
12595
}
12696

12797
authentication.setAuthenticated(true);
12898
return new OAuth2AccessTokenAuthenticationToken(
129-
registeredClient,
99+
opaqueGrantTypeTokenValidationResult.getRegisteredClient(),
130100
getAuthenticatedClientElseThrowInvalidClient(authentication),
131101
oAuth2Authorization.getAccessToken().getToken(),
132102
oAuth2Authorization.getRefreshToken() != null ? oAuth2Authorization.getRefreshToken().getToken() : null,
@@ -160,4 +130,9 @@ private OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidCl
160130
}
161131
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
162132
}
133+
134+
public void setAuthenticationValidator(Function<Map<String, Object>, OpaqueGrantTypeTokenValidationResult> authenticationValidator) {
135+
Assert.notNull(authenticationValidator, "authenticationValidator cannot be null");
136+
this.authenticationValidator = authenticationValidator;
137+
}
163138
}

lib/src/main/java/io/github/patternhelloworld/securityhelper/oauth2/api/config/security/serivce/persistence/authorization/OAuth2AuthorizationServiceImpl.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import io.github.patternhelloworld.securityhelper.oauth2.api.config.security.aop.SecurityPointCut;
55
import io.github.patternhelloworld.securityhelper.oauth2.api.config.security.dao.EasyPlusAuthorizationRepository;
66
import io.github.patternhelloworld.securityhelper.oauth2.api.config.security.entity.EasyPlusAuthorization;
7-
import io.github.patternhelloworld.securityhelper.oauth2.api.config.security.serivce.persistence.client.RegisteredClientRepositoryImpl;
7+
import io.github.patternhelloworld.securityhelper.oauth2.api.config.security.serivce.persistence.client.CacheableRegisteredClientRepositoryImpl;
88
import io.github.patternhelloworld.securityhelper.oauth2.api.config.security.token.generator.CustomAuthenticationKeyGenerator;
99
import io.github.patternhelloworld.securityhelper.oauth2.api.config.util.EasyPlusHttpHeaders;
1010
import jakarta.annotation.Nullable;
@@ -47,7 +47,7 @@ public class OAuth2AuthorizationServiceImpl implements OAuth2AuthorizationServic
4747

4848
private final EasyPlusAuthorizationRepository easyPlusAuthorizationRepository;
4949
private final SecurityPointCut securityPointCut;
50-
private final RegisteredClientRepositoryImpl registeredClientRepositoryImpl;
50+
private final CacheableRegisteredClientRepositoryImpl cacheableRegisteredClientRepositoryImpl;
5151

5252

5353
/*

0 commit comments

Comments
 (0)