Skip to content

Commit 4efac62

Browse files
authored
fix implemented (#59)
* fix implemented * fix tests * new test * new validations * fix in auth response * fix in handleAuthorizationCodeGrant * delete from cache * adapt auth serve config * fixes and tests * fix * changes * changes * change for public client * fix in redirectUri * xivatos * change * new fixes * test fixes * new test * new testing * new pkce tests * more tests * sonar issues * sonar issues fix
1 parent bd80ad3 commit 4efac62

11 files changed

+649
-30
lines changed

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ plugins {
1010
}
1111

1212
group = 'es.in2'
13-
version = '2.0.0'
13+
version = '2.0.1'
1414

1515
java {
1616
toolchain {

src/main/java/es/in2/vcverifier/model/AuthorizationContext.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ public record AuthorizationContext(
99
String redirectUri,
1010
String clientNonce,
1111
String originalRequestURL,
12-
String requestUri
12+
String requestUri,
13+
String codeChallenge,
14+
String codeChallengeMethod
1315
) {
1416
}

src/main/java/es/in2/vcverifier/security/AuthorizationServerConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
8383
)
8484
.tokenEndpoint(tokenEndpoint ->
8585
tokenEndpoint
86-
.accessTokenRequestConverter(new CustomTokenRequestConverter(jwtService, clientAssertionValidationService, vpService, cacheStoreForAuthorizationCodeData,oAuth2AuthorizationService(),objectMapper, refreshTokenDataCacheCacheStore))
86+
.accessTokenRequestConverter(new CustomTokenRequestConverter(jwtService, clientAssertionValidationService, vpService, cacheStoreForAuthorizationCodeData,objectMapper, refreshTokenDataCacheCacheStore))
8787
.authenticationProvider(new CustomAuthenticationProvider(jwtService,registeredClientRepository,backendConfig,objectMapper, refreshTokenDataCacheCacheStore, oAuth2AuthorizationService()))
8888
)
8989
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0

src/main/java/es/in2/vcverifier/security/filters/CustomAuthenticationProvider.java

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,16 @@
3030
import org.springframework.security.core.AuthenticationException;
3131
import org.springframework.security.oauth2.core.*;
3232
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
33+
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
3334
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
3435
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
36+
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
3537
import org.springframework.security.oauth2.server.authorization.authentication.*;
3638
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
3739
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
3840

41+
import java.nio.charset.StandardCharsets;
42+
import java.security.MessageDigest;
3943
import java.security.Principal;
4044
import java.security.SecureRandom;
4145
import java.time.Instant;
@@ -75,6 +79,14 @@ private Authentication handleGrant(
7579
RegisteredClient registeredClient = getRegisteredClient(clientId);
7680
log.debug("CustomAuthenticationProvider -- handleGrant -- Registered client found: {}", registeredClient);
7781

82+
if (authentication instanceof OAuth2AuthorizationCodeAuthenticationToken authCodeToken) {
83+
if (isPublicPkceClient(registeredClient)) {
84+
validateAuthorizationCodePkce(authCodeToken, clientId);
85+
} else {
86+
log.debug("Omitting redirect+PKCE validation for confidential client '{}'", clientId);
87+
}
88+
}
89+
7890
Instant issueTime = Instant.now();
7991
Instant expirationTime = issueTime.plus(
8092
Long.parseLong(ACCESS_TOKEN_EXPIRATION_TIME),
@@ -123,9 +135,81 @@ private Authentication handleGrant(
123135
}
124136

125137
log.info("Authorization grant successfully processed");
138+
139+
if (authentication instanceof OAuth2AuthorizationCodeAuthenticationToken authCodeToken) {
140+
OAuth2Authorization authToRemove =
141+
oAuth2AuthorizationService.findByToken(authCodeToken.getCode(),
142+
new OAuth2TokenType(OAuth2ParameterNames.CODE));
143+
if (authToRemove != null) {
144+
oAuth2AuthorizationService.remove(authToRemove);
145+
}
146+
}
126147
return new OAuth2AccessTokenAuthenticationToken(registeredClient, authentication, oAuth2AccessToken, oAuth2RefreshToken, additionalParameters);
127148
}
128149

150+
private boolean isPublicPkceClient(RegisteredClient rc) {
151+
if (rc == null) return false;
152+
boolean isPublic = rc.getClientAuthenticationMethods().size() == 1
153+
&& rc.getClientAuthenticationMethods().contains(ClientAuthenticationMethod.NONE);
154+
boolean requirePkce = rc.getClientSettings() != null && rc.getClientSettings().isRequireProofKey();
155+
return isPublic && requirePkce;
156+
}
157+
158+
159+
private void validateAuthorizationCodePkce(OAuth2AuthorizationCodeAuthenticationToken authCodeToken, String requestedClientId) {
160+
final String code = authCodeToken.getCode();
161+
162+
OAuth2Authorization authorization = oAuth2AuthorizationService.findByToken(code, new OAuth2TokenType(OAuth2ParameterNames.CODE));
163+
if (authorization == null) invalidGrant();
164+
165+
String storedClientId = authorization.getAttribute(OAuth2ParameterNames.CLIENT_ID);
166+
if (!Objects.equals(storedClientId, requestedClientId)) invalidGrant();
167+
168+
String storedChallenge = authorization.getAttribute(PkceParameterNames.CODE_CHALLENGE);
169+
String storedMethod = authorization.getAttribute(PkceParameterNames.CODE_CHALLENGE_METHOD);
170+
171+
boolean requirePkce = Optional.ofNullable(registeredClientRepository.findByClientId(storedClientId))
172+
.map(RegisteredClient::getClientSettings)
173+
.map(cs -> cs.isRequireProofKey())
174+
.orElse(false);
175+
176+
if (!org.springframework.util.StringUtils.hasText(storedChallenge)) {
177+
if (requirePkce) invalidGrant();
178+
return;
179+
}
180+
181+
String codeVerifier = (String) authCodeToken.getAdditionalParameters().get(PkceParameterNames.CODE_VERIFIER);
182+
if (!org.springframework.util.StringUtils.hasText(codeVerifier)) invalidGrant();
183+
184+
String method = (storedMethod == null ? "S256" : storedMethod).toUpperCase(Locale.ROOT);
185+
switch (method) {
186+
case "S256" -> {
187+
String computed = s256(codeVerifier);
188+
if (!computed.equals(storedChallenge)) invalidGrant();
189+
}
190+
case "PLAIN" -> {
191+
if (!codeVerifier.equals(storedChallenge)) invalidGrant();
192+
}
193+
default -> invalidGrant();
194+
}
195+
}
196+
197+
private static void invalidGrant() {
198+
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
199+
}
200+
201+
private static String s256(String verifier) {
202+
try {
203+
byte[] digest = MessageDigest.getInstance("SHA-256")
204+
.digest(verifier.getBytes(StandardCharsets.US_ASCII));
205+
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
206+
} catch (Exception e) {
207+
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
208+
}
209+
}
210+
211+
212+
129213
private OAuth2RefreshToken getOAuth2RefreshToken(OAuth2AuthorizationGrantAuthenticationToken authentication, Instant issueTime, String clientId, JsonNode credentialJson, RegisteredClient registeredClient) {
130214
OAuth2RefreshToken oAuth2RefreshToken;
131215
oAuth2RefreshToken = generateRefreshToken(issueTime);

src/main/java/es/in2/vcverifier/security/filters/CustomAuthorizationRequestConverter.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
2121
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
2222
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
23+
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
2324
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
2425
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
2526
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
@@ -72,12 +73,16 @@ public Authentication convert(HttpServletRequest request) {
7273
String scope = request.getParameter(OAuth2ParameterNames.SCOPE);
7374
String redirectUri = request.getParameter(OAuth2ParameterNames.REDIRECT_URI);
7475
String clientNonce = request.getParameter(NONCE);
76+
String codeChallenge = request.getParameter(PkceParameterNames.CODE_CHALLENGE);
77+
String codeChallengeMethod = request.getParameter(PkceParameterNames.CODE_CHALLENGE_METHOD);
7578
AuthorizationContext authorizationContext = AuthorizationContext.builder()
7679
.requestUri(requestUri)
7780
.state(state)
7881
.originalRequestURL(originalRequestURL)
7982
.redirectUri(redirectUri)
8083
.clientNonce(clientNonce)
84+
.codeChallenge(codeChallenge)
85+
.codeChallengeMethod(codeChallengeMethod)
8186
.scope(scope)
8287
.build();
8388

@@ -482,9 +487,16 @@ private void cacheAuthorizationRequest(AuthorizationContext authorizationContext
482487
if (nonce != null && !nonce.isBlank()) {
483488
additionalParameters.put(NONCE, nonce);
484489
}
485-
builder.additionalParameters(additionalParameters);
486-
490+
String codeChallenge = authorizationContext.codeChallenge();
491+
if (codeChallenge != null && !codeChallenge.isBlank()) {
492+
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge);
493+
}
494+
String codeChallengeMethod = authorizationContext.codeChallengeMethod();
495+
if (codeChallengeMethod != null && !codeChallengeMethod.isBlank()) {
496+
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, codeChallengeMethod);
497+
}
487498

499+
builder.additionalParameters(additionalParameters);
488500

489501
// Build the request
490502
OAuth2AuthorizationRequest oAuth2AuthorizationRequest = builder.build();
@@ -505,4 +517,4 @@ private String getFullRequestUrl(HttpServletRequest request) {
505517
}
506518
return requestURL.toString();
507519
}
508-
}
520+
}

src/main/java/es/in2/vcverifier/security/filters/CustomTokenRequestConverter.java

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
2525
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
2626
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
27-
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
27+
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
2828
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationToken;
2929
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken;
3030
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationToken;
@@ -49,7 +49,6 @@ public class CustomTokenRequestConverter implements AuthenticationConverter {
4949
private final ClientAssertionValidationService clientAssertionValidationService;
5050
private final VpService vpService;
5151
private final CacheStore<AuthorizationCodeData> cacheStoreForAuthorizationCodeData;
52-
private final OAuth2AuthorizationService oAuth2AuthorizationService;
5352
private final ObjectMapper objectMapper;
5453
private final CacheStore<RefreshTokenDataCache> refreshTokenDataCacheCacheStore;
5554

@@ -74,15 +73,11 @@ private Authentication handleAuthorizationCodeGrant(MultiValueMap<String, String
7473
String code = parameters.getFirst(OAuth2ParameterNames.CODE);
7574
String state = parameters.getFirst(OAuth2ParameterNames.STATE);
7675
String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);
76+
String redirectUri = parameters.getFirst(OAuth2ParameterNames.REDIRECT_URI);
77+
String codeVerifier = parameters.getFirst(PkceParameterNames.CODE_VERIFIER);
7778

7879
AuthorizationCodeData authorizationCodeData = cacheStoreForAuthorizationCodeData.get(code);
7980

80-
// Remove the code from cache after retrieving the object
81-
cacheStoreForAuthorizationCodeData.delete(code);
82-
83-
// Remove the authorization from the initial request
84-
oAuth2AuthorizationService.remove(authorizationCodeData.oAuth2Authorization());
85-
8681
// Check state only if it is not null and not blank
8782
if (state != null && !state.isBlank() && (!authorizationCodeData.state().equals(state))) {
8883
log.error("CustomTokenRequestConverter -- handleAuthorizationCodeGrant -- State mismatch. Expected: {}, Actual: {}",
@@ -106,8 +101,12 @@ private Authentication handleAuthorizationCodeGrant(MultiValueMap<String, String
106101
additionalParameters.put(NONCE, nonce);
107102
}
108103

104+
if (codeVerifier != null && !codeVerifier.isBlank()) {
105+
additionalParameters.put(PkceParameterNames.CODE_VERIFIER, codeVerifier);
106+
}
107+
109108
// Return the authentication token
110-
return new OAuth2AuthorizationCodeAuthenticationToken(code, clientPrincipal, null, additionalParameters);
109+
return new OAuth2AuthorizationCodeAuthenticationToken(code, clientPrincipal, redirectUri, additionalParameters);
111110
}
112111

113112
private Authentication handleClientCredentialsGrant(MultiValueMap<String, String> parameters) {
@@ -194,4 +193,4 @@ private static MultiValueMap<String, String> getParameters(HttpServletRequest re
194193
return parameters;
195194
}
196195

197-
}
196+
}

src/main/java/es/in2/vcverifier/service/impl/AuthorizationResponseProcessorServiceImpl.java

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
1616
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
1717
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
18+
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
19+
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
1820
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
1921
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
2022
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
@@ -95,15 +97,33 @@ public void processAuthResponse(String state, String vpToken){
9597
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
9698
}
9799

100+
101+
var addl = oAuth2AuthorizationRequest.getAdditionalParameters();
102+
String codeChallenge = (String) addl.get(PkceParameterNames.CODE_CHALLENGE);
103+
String codeChallengeMethod = (String) addl.get(PkceParameterNames.CODE_CHALLENGE_METHOD);
104+
105+
98106
Instant expirationTime = issueTime.plus(Long.parseLong(ACCESS_TOKEN_EXPIRATION_TIME), ChronoUnit.valueOf(ACCESS_TOKEN_EXPIRATION_CHRONO_UNIT));
99107
// Register the Oauth2Authorization because is needed for verifications
100-
OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
108+
OAuth2Authorization.Builder authBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
101109
.id(registeredClient.getId())
102110
.principalName(registeredClient.getClientId())
103111
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
104112
.token(new OAuth2AuthorizationCode(code, issueTime, expirationTime))
105-
.attribute(OAuth2AuthorizationRequest.class.getName(), oAuth2AuthorizationRequest)
106-
.build();
113+
.attribute(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
114+
.attribute(OAuth2ParameterNames.REDIRECT_URI, oAuth2AuthorizationRequest.getRedirectUri())
115+
.attribute(OAuth2ParameterNames.SCOPE, String.join(" ", oAuth2AuthorizationRequest.getScopes()))
116+
.attribute(OAuth2AuthorizationRequest.class.getName(), oAuth2AuthorizationRequest);
117+
118+
if (org.springframework.util.StringUtils.hasText(codeChallenge)) {
119+
authBuilder.attribute(PkceParameterNames.CODE_CHALLENGE, codeChallenge);
120+
}
121+
if (org.springframework.util.StringUtils.hasText(codeChallengeMethod)) {
122+
authBuilder.attribute(PkceParameterNames.CODE_CHALLENGE_METHOD, codeChallengeMethod);
123+
}
124+
125+
OAuth2Authorization authorization = authBuilder.build();
126+
oAuth2AuthorizationService.save(authorization);
107127

108128
log.info("OAuth2Authorization generated");
109129

@@ -123,7 +143,6 @@ public void processAuthResponse(String state, String vpToken){
123143
AuthorizationCodeData authorizationCodeData = authCodeDataBuilder.build();
124144
cacheStoreForAuthorizationCodeData.add(code, authorizationCodeData);
125145

126-
oAuth2AuthorizationService.save(authorization);
127146

128147
// Build the redirect URL with the code (code) and the state
129148
String redirectUrl = UriComponentsBuilder.fromHttpUrl(redirectUri)
@@ -174,6 +193,4 @@ private void validateVpTokenNonceAndAudience(String decodedVpToken, String state
174193
}
175194
}
176195

177-
}
178-
179-
196+
}

0 commit comments

Comments
 (0)