From 5c31fb1b7e7a0efbb60cb7aa34762ad5577eba45 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Fri, 2 Oct 2020 05:02:52 -0400 Subject: [PATCH] Move PKCE to OAuth2ClientAuthenticationProvider PR gh-93 --- .../OAuth2AuthorizationServerConfigurer.java | 3 +- ...thorizationCodeAuthenticationProvider.java | 63 +--- ...2AuthorizationCodeAuthenticationToken.java | 38 +- .../OAuth2ClientAuthenticationProvider.java | 106 +++++- .../OAuth2ClientAuthenticationToken.java | 32 +- ...entSecretBasicAuthenticationConverter.java | 14 +- .../DelegatingAuthenticationConverter.java | 64 ++++ .../web/OAuth2ClientAuthenticationFilter.java | 6 +- .../web/OAuth2EndpointUtils.java | 10 + .../web/OAuth2TokenEndpointFilter.java | 34 +- .../PublicClientAuthenticationConverter.java | 72 ++++ .../OAuth2AuthorizationCodeGrantTests.java | 5 +- ...zationCodeAuthenticationProviderTests.java | 313 +--------------- ...orizationCodeAuthenticationTokenTests.java | 32 +- ...uth2ClientAuthenticationProviderTests.java | 333 +++++++++++++++++- .../OAuth2ClientAuthenticationTokenTests.java | 21 +- ...redentialsAuthenticationProviderTests.java | 2 +- ...cretBasicAuthenticationConverterTests.java | 26 ++ ...OAuth2ClientAuthenticationFilterTests.java | 4 +- .../web/OAuth2TokenEndpointFilterTests.java | 32 -- ...licClientAuthenticationConverterTests.java | 97 +++++ 21 files changed, 774 insertions(+), 533 deletions(-) create mode 100644 oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/DelegatingAuthenticationConverter.java create mode 100644 oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/PublicClientAuthenticationConverter.java create mode 100644 oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/PublicClientAuthenticationConverterTests.java diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java index 8a4e037..778f31a 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java @@ -120,7 +120,8 @@ public final class OAuth2AuthorizationServerConfigurer additionalParameters; @@ -58,32 +57,6 @@ public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenti Assert.notNull(clientPrincipal, "clientPrincipal cannot be null"); this.code = code; this.clientPrincipal = clientPrincipal; - this.clientId = OAuth2ClientAuthenticationToken.class.isAssignableFrom(this.clientPrincipal.getClass()) ? - (String) this.clientPrincipal.getPrincipal() : - null; - this.redirectUri = redirectUri; - this.additionalParameters = Collections.unmodifiableMap( - additionalParameters != null ? - additionalParameters : - Collections.emptyMap()); - } - - /** - * Constructs an {@code OAuth2AuthorizationCodeAuthenticationToken} using the provided parameters. - * - * @param code the authorization code - * @param clientId the client identifier - * @param redirectUri the redirect uri - * @param additionalParameters the additional parameters - */ - public OAuth2AuthorizationCodeAuthenticationToken(String code, String clientId, - @Nullable String redirectUri, @Nullable Map additionalParameters) { - super(Collections.emptyList()); - Assert.hasText(code, "code cannot be empty"); - Assert.hasText(clientId, "clientId cannot be empty"); - this.code = code; - this.clientPrincipal = null; - this.clientId = clientId; this.redirectUri = redirectUri; this.additionalParameters = Collections.unmodifiableMap( additionalParameters != null ? @@ -93,7 +66,7 @@ public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenti @Override public Object getPrincipal() { - return this.clientPrincipal != null ? this.clientPrincipal : this.clientId; + return this.clientPrincipal; } @Override @@ -110,15 +83,6 @@ public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenti return this.code; } - /** - * Returns the client identifier - * - * @return the client identifier - */ - public @Nullable String getClientId() { - return this.clientId; - } - /** * Returns the redirect uri. * diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProvider.java index 57e6a64..1738029 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProvider.java @@ -18,49 +18,80 @@ package org.springframework.security.oauth2.server.authorization.authentication; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationAttributeNames; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.TokenType; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.Map; /** - * An {@link AuthenticationProvider} implementation that validates {@link OAuth2ClientAuthenticationToken}'s. + * An {@link AuthenticationProvider} implementation used for authenticating an OAuth 2.0 Client. * * @author Joe Grandja * @author Patryk Kostrzewa + * @author Daniel Garnier-Moiroux * @since 0.0.1 * @see AuthenticationProvider * @see OAuth2ClientAuthenticationToken * @see RegisteredClientRepository + * @see OAuth2AuthorizationService */ public class OAuth2ClientAuthenticationProvider implements AuthenticationProvider { private final RegisteredClientRepository registeredClientRepository; + private final OAuth2AuthorizationService authorizationService; /** * Constructs an {@code OAuth2ClientAuthenticationProvider} using the provided parameters. * * @param registeredClientRepository the repository of registered clients + * @param authorizationService the authorization service */ - public OAuth2ClientAuthenticationProvider(RegisteredClientRepository registeredClientRepository) { + public OAuth2ClientAuthenticationProvider(RegisteredClientRepository registeredClientRepository, + OAuth2AuthorizationService authorizationService) { Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null"); + Assert.notNull(authorizationService, "authorizationService cannot be null"); this.registeredClientRepository = registeredClientRepository; + this.authorizationService = authorizationService; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { - String clientId = authentication.getPrincipal().toString(); + OAuth2ClientAuthenticationToken clientAuthentication = + (OAuth2ClientAuthenticationToken) authentication; + + String clientId = clientAuthentication.getPrincipal().toString(); RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId); if (registeredClient == null) { - throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT)); + throwInvalidClient(); } - String clientSecret = authentication.getCredentials().toString(); - if (!registeredClient.getClientSecret().equals(clientSecret)) { - throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT)); + if (clientAuthentication.getCredentials() != null) { + String clientSecret = clientAuthentication.getCredentials().toString(); + // TODO Use PasswordEncoder.matches() + if (!registeredClient.getClientSecret().equals(clientSecret)) { + throwInvalidClient(); + } } + authenticatePkceIfAvailable(clientAuthentication, registeredClient); + return new OAuth2ClientAuthenticationToken(registeredClient); } @@ -68,4 +99,65 @@ public class OAuth2ClientAuthenticationProvider implements AuthenticationProvide public boolean supports(Class authentication) { return OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication); } + + private void authenticatePkceIfAvailable(OAuth2ClientAuthenticationToken clientAuthentication, + RegisteredClient registeredClient) { + + Map parameters = clientAuthentication.getAdditionalParameters(); + if (CollectionUtils.isEmpty(parameters) || !authorizationCodeGrant(parameters)) { + return; + } + + OAuth2Authorization authorization = this.authorizationService.findByToken( + (String) parameters.get(OAuth2ParameterNames.CODE), + TokenType.AUTHORIZATION_CODE); + if (authorization == null) { + throwInvalidClient(); + } + + OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute( + OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST); + + String codeChallenge = (String) authorizationRequest.getAdditionalParameters() + .get(PkceParameterNames.CODE_CHALLENGE); + if (StringUtils.hasText(codeChallenge)) { + String codeChallengeMethod = (String) authorizationRequest.getAdditionalParameters() + .get(PkceParameterNames.CODE_CHALLENGE_METHOD); + String codeVerifier = (String) parameters.get(PkceParameterNames.CODE_VERIFIER); + if (!codeVerifierValid(codeVerifier, codeChallenge, codeChallengeMethod)) { + throwInvalidClient(); + } + } else if (registeredClient.getClientSettings().requireProofKey()) { + throwInvalidClient(); + } + } + + private static boolean authorizationCodeGrant(Map parameters) { + return AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals( + parameters.get(OAuth2ParameterNames.GRANT_TYPE)) && + parameters.get(OAuth2ParameterNames.CODE) != null; + } + + private static boolean codeVerifierValid(String codeVerifier, String codeChallenge, String codeChallengeMethod) { + if (!StringUtils.hasText(codeVerifier)) { + return false; + } else if (!StringUtils.hasText(codeChallengeMethod) || "plain".equals(codeChallengeMethod)) { + return codeVerifier.equals(codeChallenge); + } else if ("S256".equals(codeChallengeMethod)) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII)); + String encodedVerifier = Base64.getUrlEncoder().withoutPadding().encodeToString(digest); + return encodedVerifier.equals(codeChallenge); + } catch (NoSuchAlgorithmException ex) { + // It is unlikely that SHA-256 is not available on the server. If it is not available, + // there will likely be bigger issues as well. We default to SERVER_ERROR. + } + } + throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR)); + } + + private static void throwInvalidClient() { + throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT)); + } } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationToken.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationToken.java index cc5ffa7..41fe7c5 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationToken.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationToken.java @@ -23,6 +23,7 @@ import org.springframework.security.oauth2.server.authorization.client.Registere import org.springframework.util.Assert; import java.util.Collections; +import java.util.Map; /** * An {@link Authentication} implementation used for OAuth 2.0 Client Authentication. @@ -38,6 +39,7 @@ public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken private static final long serialVersionUID = SpringSecurityCoreVersion2.SERIAL_VERSION_UID; private String clientId; private String clientSecret; + private Map additionalParameters; private RegisteredClient registeredClient; /** @@ -45,13 +47,28 @@ public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken * * @param clientId the client identifier * @param clientSecret the client secret + * @param additionalParameters the additional parameters */ - public OAuth2ClientAuthenticationToken(String clientId, String clientSecret) { + public OAuth2ClientAuthenticationToken(String clientId, String clientSecret, + @Nullable Map additionalParameters) { + this(clientId, additionalParameters); + Assert.hasText(clientSecret, "clientSecret cannot be empty"); + this.clientSecret = clientSecret; + } + + /** + * Constructs an {@code OAuth2ClientAuthenticationToken} using the provided parameters. + * + * @param clientId the client identifier + * @param additionalParameters the additional parameters + */ + public OAuth2ClientAuthenticationToken(String clientId, + @Nullable Map additionalParameters) { super(Collections.emptyList()); Assert.hasText(clientId, "clientId cannot be empty"); - Assert.hasText(clientSecret, "clientSecret cannot be empty"); this.clientId = clientId; - this.clientSecret = clientSecret; + this.additionalParameters = additionalParameters != null ? + Collections.unmodifiableMap(additionalParameters) : null; } /** @@ -78,6 +95,15 @@ public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken return this.clientSecret; } + /** + * Returns the additional parameters + * + * @return the additional parameters + */ + public @Nullable Map getAdditionalParameters() { + return this.additionalParameters; + } + /** * Returns the {@link RegisteredClient registered client}. * diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/ClientSecretBasicAuthenticationConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/ClientSecretBasicAuthenticationConverter.java index c18cfbb..b0ef010 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/ClientSecretBasicAuthenticationConverter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/ClientSecretBasicAuthenticationConverter.java @@ -28,6 +28,9 @@ import javax.servlet.http.HttpServletRequest; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; /** * Attempts to extract HTTP Basic credentials from {@link HttpServletRequest} @@ -82,6 +85,15 @@ public class ClientSecretBasicAuthenticationConverter implements AuthenticationC throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST), ex); } - return new OAuth2ClientAuthenticationToken(clientID, clientSecret); + return new OAuth2ClientAuthenticationToken(clientID, clientSecret, extractAdditionalParameters(request)); + } + + private static Map extractAdditionalParameters(HttpServletRequest request) { + Map additionalParameters = Collections.emptyMap(); + if (OAuth2EndpointUtils.matchesPkceTokenRequest(request)) { + // Confidential clients can also leverage PKCE + additionalParameters = new HashMap<>(OAuth2EndpointUtils.getParameters(request).toSingleValueMap()); + } + return additionalParameters; } } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/DelegatingAuthenticationConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/DelegatingAuthenticationConverter.java new file mode 100644 index 0000000..44500b9 --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/DelegatingAuthenticationConverter.java @@ -0,0 +1,64 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.server.authorization.web; + +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationConverter; +import org.springframework.util.Assert; + +import javax.servlet.http.HttpServletRequest; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; + +/** + * An {@link AuthenticationConverter} that simply delegates to it's + * internal {@code List} of {@link AuthenticationConverter}(s). + *

+ * Each {@link AuthenticationConverter} is given a chance to + * {@link AuthenticationConverter#convert(HttpServletRequest)} + * with the first {@code non-null} {@link Authentication} being returned. + * + * @author Joe Grandja + * @since 0.0.2 + * @see AuthenticationConverter + */ +public final class DelegatingAuthenticationConverter implements AuthenticationConverter { + private final List converters; + + /** + * Constructs a {@code DelegatingAuthenticationConverter} using the provided parameters. + * + * @param converters a {@code List} of {@link AuthenticationConverter}(s) + */ + public DelegatingAuthenticationConverter(List converters) { + Assert.notEmpty(converters, "converters cannot be empty"); + this.converters = Collections.unmodifiableList(new LinkedList<>(converters)); + } + + @Override + public Authentication convert(HttpServletRequest request) { + Assert.notNull(request, "request cannot be null"); + // @formatter:off + return this.converters.stream() + .map(converter -> converter.convert(request)) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + // @formatter:on + } +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java index 5a3cf49..280cda9 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java @@ -41,6 +41,7 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.Arrays; /** * A {@code Filter} that processes an authentication request for an OAuth 2.0 Client. @@ -73,7 +74,10 @@ public class OAuth2ClientAuthenticationFilter extends OncePerRequestFilter { Assert.notNull(requestMatcher, "requestMatcher cannot be null"); this.authenticationManager = authenticationManager; this.requestMatcher = requestMatcher; - this.authenticationConverter = new ClientSecretBasicAuthenticationConverter(); + this.authenticationConverter = new DelegatingAuthenticationConverter( + Arrays.asList( + new ClientSecretBasicAuthenticationConverter(), + new PublicClientAuthenticationConverter())); this.authenticationSuccessHandler = this::onAuthenticationSuccess; this.authenticationFailureHandler = this::onAuthenticationFailure; } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2EndpointUtils.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2EndpointUtils.java index adad007..ea33ba0 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2EndpointUtils.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2EndpointUtils.java @@ -15,6 +15,9 @@ */ package org.springframework.security.oauth2.server.authorization.web; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -46,4 +49,11 @@ final class OAuth2EndpointUtils { }); return parameters; } + + static boolean matchesPkceTokenRequest(HttpServletRequest request) { + return AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals( + request.getParameter(OAuth2ParameterNames.GRANT_TYPE)) && + request.getParameter(OAuth2ParameterNames.CODE) != null && + request.getParameter(PkceParameterNames.CODE_VERIFIER) != null; + } } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilter.java index ef14b48..568991f 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilter.java @@ -30,13 +30,11 @@ import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; -import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationToken; -import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; @@ -189,12 +187,6 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter { throw new OAuth2AuthenticationException(error); } - private static boolean isClientAuthenticated(Authentication clientPrincipal) { - return clientPrincipal != null && - OAuth2ClientAuthenticationToken.class.isAssignableFrom(clientPrincipal.getClass()) && - clientPrincipal.isAuthenticated(); - } - private static class AuthorizationCodeAuthenticationConverter implements Converter { @Override @@ -205,6 +197,8 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter { return null; } + Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication(); + MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request); // code (REQUIRED) @@ -222,25 +216,6 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter { throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI); } - // client_id (REQUIRED) - // Required only if the client did not authenticate - String clientId = null; - Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication(); - if (!isClientAuthenticated(clientPrincipal)) { - clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID); - if (!StringUtils.hasText(clientId) || - parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) { - throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID); - } - - // code_verifier (REQUIRED for public clients) - String codeVerifier = parameters.getFirst(PkceParameterNames.CODE_VERIFIER); - if (!StringUtils.hasText(codeVerifier) || - parameters.get(PkceParameterNames.CODE_VERIFIER).size() != 1) { - throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_VERIFIER); - } - } - Map additionalParameters = parameters .entrySet() .stream() @@ -250,10 +225,7 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter { !e.getKey().equals(OAuth2ParameterNames.REDIRECT_URI)) .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0))); - - return clientId != null ? - new OAuth2AuthorizationCodeAuthenticationToken(code, clientId, redirectUri, additionalParameters) : - new OAuth2AuthorizationCodeAuthenticationToken(code, clientPrincipal, redirectUri, additionalParameters); + return new OAuth2AuthorizationCodeAuthenticationToken(code, clientPrincipal, redirectUri, additionalParameters); } } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/PublicClientAuthenticationConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/PublicClientAuthenticationConverter.java new file mode 100644 index 0000000..507e123 --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/PublicClientAuthenticationConverter.java @@ -0,0 +1,72 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.server.authorization.web; + +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.web.authentication.AuthenticationConverter; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; + +/** + * Attempts to extract the parameters from {@link HttpServletRequest} + * used for authenticating public clients using Proof Key for Code Exchange (PKCE). + * + * @author Joe Grandja + * @since 0.0.2 + * @see AuthenticationConverter + * @see OAuth2ClientAuthenticationToken + * @see OAuth2ClientAuthenticationFilter + * @see Proof Key for Code Exchange by OAuth Public Clients + */ +public class PublicClientAuthenticationConverter implements AuthenticationConverter { + + @Override + public Authentication convert(HttpServletRequest request) { + if (!OAuth2EndpointUtils.matchesPkceTokenRequest(request)) { + return null; + } + + MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request); + + // client_id (REQUIRED for public clients) + String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID); + if (!StringUtils.hasText(clientId)) { + return null; + } + if (parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) { + throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST)); + } + + // code_verifier (REQUIRED) + if (parameters.get(PkceParameterNames.CODE_VERIFIER).size() != 1) { + throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST)); + } + + parameters.remove(OAuth2ParameterNames.CLIENT_ID); + + return new OAuth2ClientAuthenticationToken( + clientId, new HashMap<>(parameters.toSingleValueMap())); + } +} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationCodeGrantTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationCodeGrantTests.java index f35a8a3..9fb80c9 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationCodeGrantTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationCodeGrantTests.java @@ -207,13 +207,12 @@ public class OAuth2AuthorizationCodeGrantTests { this.mvc.perform(post(OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI) .params(getTokenRequestParameters(registeredClient, authorization)) .param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId()) - .param(PkceParameterNames.CODE_VERIFIER, S256_CODE_VERIFIER) - .with(user("user"))) // TODO Remove after PKCE authentication is moved to OAuth2ClientAuthenticationProvider + .param(PkceParameterNames.CODE_VERIFIER, S256_CODE_VERIFIER)) .andExpect(status().isOk()) .andExpect(jsonPath("$.access_token").isNotEmpty()); verify(registeredClientRepository, times(2)).findByClientId(eq(registeredClient.getClientId())); - verify(authorizationService).findByToken( + verify(authorizationService, times(2)).findByToken( eq(authorization.getAttribute(OAuth2AuthorizationAttributeNames.CODE)), eq(TokenType.AUTHORIZATION_CODE)); verify(authorizationService, times(2)).save(any()); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java index efddc09..4c6fce9 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java @@ -22,7 +22,6 @@ import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; -import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; import org.springframework.security.oauth2.jose.JoseHeaderNames; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.security.oauth2.jwt.Jwt; @@ -36,13 +35,9 @@ import org.springframework.security.oauth2.server.authorization.client.InMemoryR import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; -import org.springframework.security.oauth2.server.authorization.config.ClientSettings; import java.time.Instant; import java.time.temporal.ChronoUnit; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -59,19 +54,8 @@ import static org.mockito.Mockito.when; * @author Daniel Garnier-Moiroux */ public class OAuth2AuthorizationCodeAuthenticationProviderTests { - private static final String PLAIN_CODE_VERIFIER = "pkce-key"; - private static final String PLAIN_CODE_CHALLENGE = PLAIN_CODE_VERIFIER; - - // See RFC 7636: Appendix B. Example for the S256 code_challenge_method - // https://tools.ietf.org/html/rfc7636#appendix-B - private static final String S256_CODE_VERIFIER = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"; - private static final String S256_CODE_CHALLENGE = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"; - private static final String AUTHORIZATION_CODE = "code"; - private RegisteredClient registeredClient; - private RegisteredClient otherRegisteredClient; - private RegisteredClient registeredClientRequiresProofKey; private RegisteredClientRepository registeredClientRepository; private OAuth2AuthorizationService authorizationService; private JwtEncoder jwtEncoder; @@ -80,17 +64,7 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests { @Before public void setUp() { this.registeredClient = TestRegisteredClients.registeredClient().build(); - this.otherRegisteredClient = TestRegisteredClients.registeredClient2().build(); - this.registeredClientRequiresProofKey = TestRegisteredClients.registeredClient() - .id("registration-3") - .clientId("client-3") - .clientSettings(new ClientSettings().requireProofKey(true)) - .build(); - this.registeredClientRepository = new InMemoryRegisteredClientRepository( - this.registeredClient, - this.otherRegisteredClient, - this.registeredClientRequiresProofKey - ); + this.registeredClientRepository = new InMemoryRegisteredClientRepository(this.registeredClient); this.authorizationService = mock(OAuth2AuthorizationService.class); this.jwtEncoder = mock(JwtEncoder.class); this.authenticationProvider = new OAuth2AuthorizationCodeAuthenticationProvider( @@ -139,7 +113,7 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests { @Test public void authenticateWhenClientPrincipalNotAuthenticatedThenThrowOAuth2AuthenticationException() { OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken( - this.registeredClient.getClientId(), this.registeredClient.getClientSecret()); + this.registeredClient.getClientId(), this.registeredClient.getClientSecret(), null); OAuth2AuthorizationCodeAuthenticationToken authentication = new OAuth2AuthorizationCodeAuthenticationToken(AUTHORIZATION_CODE, clientPrincipal, null, null); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) @@ -149,31 +123,6 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests { .isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT); } - @Test - public void authenticateWhenPublicClientAndInvalidClientIdThenThrowOAuth2AuthenticationException() { - OAuth2Authorization authorization = TestOAuth2Authorizations - .authorization(this.registeredClient, createPkceParametersPlain()) - .build(); - when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE))) - .thenReturn(authorization); - - OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute( - OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST); - OAuth2AuthorizationCodeAuthenticationToken authentication = - new OAuth2AuthorizationCodeAuthenticationToken( - AUTHORIZATION_CODE, - "invalid-client-id", - authorizationRequest.getRedirectUri(), - Collections.singletonMap(PkceParameterNames.CODE_VERIFIER, PLAIN_CODE_CHALLENGE) - ); - - assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) - .isInstanceOf(OAuth2AuthenticationException.class) - .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) - .extracting("errorCode") - .isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT); - } - @Test public void authenticateWhenInvalidCodeThenThrowOAuth2AuthenticationException() { OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(this.registeredClient); @@ -203,30 +152,6 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests { .isEqualTo(OAuth2ErrorCodes.INVALID_GRANT); } - @Test - public void authenticateWhenPublicClientAndClientIdNotMatchThenThrowOAuth2AuthenticationException() { - OAuth2Authorization authorization = TestOAuth2Authorizations - .authorization(this.registeredClient, createPkceParametersPlain()) - .build(); - when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE))) - .thenReturn(authorization); - - OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute( - OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST); - OAuth2AuthorizationCodeAuthenticationToken authentication = - new OAuth2AuthorizationCodeAuthenticationToken( - AUTHORIZATION_CODE, - this.otherRegisteredClient.getClientId(), - authorizationRequest.getRedirectUri(), - Collections.singletonMap(PkceParameterNames.CODE_VERIFIER, PLAIN_CODE_VERIFIER) - ); - assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) - .isInstanceOf(OAuth2AuthenticationException.class) - .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) - .extracting("errorCode") - .isEqualTo(OAuth2ErrorCodes.INVALID_GRANT); - } - @Test public void authenticateWhenInvalidRedirectUriThenThrowOAuth2AuthenticationException() { OAuth2Authorization authorization = TestOAuth2Authorizations.authorization().build(); @@ -272,240 +197,6 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests { assertThat(accessTokenAuthentication.getAccessToken()).isEqualTo(updatedAuthorization.getAccessToken()); } - @Test - public void authenticateWhenRequireProofKeyAndMissingCodeChallengeThenThrowOAuth2AuthenticationException() { - OAuth2Authorization authorization = TestOAuth2Authorizations - .authorization(this.registeredClientRequiresProofKey) - .build(); - when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE))) - .thenReturn(authorization); - - OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute( - OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST); - OAuth2AuthorizationCodeAuthenticationToken authentication = - new OAuth2AuthorizationCodeAuthenticationToken( - AUTHORIZATION_CODE, - this.registeredClientRequiresProofKey.getClientId(), - authorizationRequest.getRedirectUri(), - Collections.singletonMap(PkceParameterNames.CODE_VERIFIER, PLAIN_CODE_VERIFIER) - ); - assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) - .isInstanceOf(OAuth2AuthenticationException.class) - .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) - .extracting("errorCode") - .isEqualTo(OAuth2ErrorCodes.INVALID_GRANT); - } - - @Test - public void authenticateWhenPublicClientAndMissingCodeVerifierThenThrowOAuth2AuthenticationException() { - OAuth2Authorization authorization = TestOAuth2Authorizations - .authorization(this.registeredClient, createPkceParametersPlain()) - .build(); - when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE))) - .thenReturn(authorization); - - OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute( - OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST); - OAuth2AuthorizationCodeAuthenticationToken authentication = - new OAuth2AuthorizationCodeAuthenticationToken( - AUTHORIZATION_CODE, - authorizationRequest.getClientId(), - authorizationRequest.getRedirectUri(), - null - ); - assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) - .isInstanceOf(OAuth2AuthenticationException.class) - .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) - .extracting("errorCode") - .isEqualTo(OAuth2ErrorCodes.INVALID_GRANT); - } - - @Test - public void authenticateWhenConfidentialClientRequireProofKeyAndMissingCodeVerifierThenThrowOAuth2AuthenticationException() { - OAuth2Authorization authorization = TestOAuth2Authorizations - .authorization(this.registeredClientRequiresProofKey, createPkceParametersPlain()) - .build(); - when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE))) - .thenReturn(authorization); - - OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken( - this.registeredClientRequiresProofKey); - OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute( - OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST); - OAuth2AuthorizationCodeAuthenticationToken authentication = - new OAuth2AuthorizationCodeAuthenticationToken( - AUTHORIZATION_CODE, - clientPrincipal, - authorizationRequest.getRedirectUri(), - null - ); - assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) - .isInstanceOf(OAuth2AuthenticationException.class) - .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) - .extracting("errorCode") - .isEqualTo(OAuth2ErrorCodes.INVALID_GRANT); - } - - @Test - public void authenticateWhenPublicClientAndPlainMethodAndInvalidCodeVerifierThenThrowOAuth2AuthenticationException() { - OAuth2Authorization authorization = TestOAuth2Authorizations - .authorization(this.registeredClient, createPkceParametersPlain()) - .build(); - when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE))) - .thenReturn(authorization); - - OAuth2AuthorizationCodeAuthenticationToken authentication = createAuthorizationCodeAuthentication( - this.registeredClient, "invalid-code-verifier"); - assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) - .isInstanceOf(OAuth2AuthenticationException.class) - .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) - .extracting("errorCode") - .isEqualTo(OAuth2ErrorCodes.INVALID_GRANT); - } - - @Test - public void authenticateWhenPublicClientAndS256MethodAndInvalidCodeVerifierThenThrowOAuth2AuthenticationException() { - OAuth2Authorization authorization = TestOAuth2Authorizations - .authorization(this.registeredClient, createPkceParametersS256()) - .build(); - when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE))) - .thenReturn(authorization); - - OAuth2AuthorizationCodeAuthenticationToken authentication = createAuthorizationCodeAuthentication( - this.registeredClient, "invalid-code-verifier"); - assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) - .isInstanceOf(OAuth2AuthenticationException.class) - .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) - .extracting("errorCode") - .isEqualTo(OAuth2ErrorCodes.INVALID_GRANT); - } - - @Test - public void authenticateWhenRequireProofKeyAndUnsupportedCodeChallengeMethodThenThrowOAuth2AuthenticationException() { - Map pkceParameters = createPkceParametersPlain(); - // This should never happen: the Authorization endpoint should not allow it - pkceParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "unsupported-challenge-method"); - OAuth2Authorization authorization = TestOAuth2Authorizations - .authorization(this.registeredClientRequiresProofKey, pkceParameters) - .build(); - when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE))) - .thenReturn(authorization); - - OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute( - OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST); - OAuth2AuthorizationCodeAuthenticationToken authentication = - new OAuth2AuthorizationCodeAuthenticationToken( - AUTHORIZATION_CODE, - this.registeredClientRequiresProofKey.getClientId(), - authorizationRequest.getRedirectUri(), - Collections.singletonMap(PkceParameterNames.CODE_VERIFIER, PLAIN_CODE_VERIFIER) - ); - assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) - .isInstanceOf(OAuth2AuthenticationException.class) - .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) - .extracting("errorCode") - .isEqualTo(OAuth2ErrorCodes.SERVER_ERROR); - } - - @Test - public void authenticateWhenPublicClientAndPlainMethodAndValidCodeVerifierThenReturnAccessToken() { - OAuth2Authorization authorization = TestOAuth2Authorizations - .authorization(this.registeredClient, createPkceParametersPlain()) - .build(); - when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE))) - .thenReturn(authorization); - when(this.jwtEncoder.encode(any(), any())).thenReturn(createJwt()); - - OAuth2AuthorizationCodeAuthenticationToken authentication = createAuthorizationCodeAuthentication( - this.registeredClient, PLAIN_CODE_VERIFIER); - - OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = - (OAuth2AccessTokenAuthenticationToken) this.authenticationProvider.authenticate(authentication); - - ArgumentCaptor authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class); - verify(this.authorizationService).save(authorizationCaptor.capture()); - OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue(); - - OAuth2ClientAuthenticationToken clientAuthentication = (OAuth2ClientAuthenticationToken) accessTokenAuthentication.getPrincipal(); - assertThat(clientAuthentication.getPrincipal()).isEqualTo(this.registeredClient.getClientId()); - assertThat(updatedAuthorization.getAccessToken()).isNotNull(); - assertThat(accessTokenAuthentication.getAccessToken()).isEqualTo(updatedAuthorization.getAccessToken()); - } - - @Test - public void authenticateWhenPublicClientAndMissingMethodThenDefaultPlainMethodAndReturnAccessToken() { - OAuth2Authorization authorization = TestOAuth2Authorizations - .authorization(this.registeredClient, Collections.singletonMap(PkceParameterNames.CODE_CHALLENGE, PLAIN_CODE_CHALLENGE)) - .build(); - when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE))) - .thenReturn(authorization); - when(this.jwtEncoder.encode(any(), any())).thenReturn(createJwt()); - - OAuth2AuthorizationCodeAuthenticationToken authentication = createAuthorizationCodeAuthentication( - this.registeredClient, PLAIN_CODE_VERIFIER); - - OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = - (OAuth2AccessTokenAuthenticationToken) this.authenticationProvider.authenticate(authentication); - - ArgumentCaptor authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class); - verify(this.authorizationService).save(authorizationCaptor.capture()); - OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue(); - - OAuth2ClientAuthenticationToken clientAuthentication = (OAuth2ClientAuthenticationToken) accessTokenAuthentication.getPrincipal(); - assertThat(clientAuthentication.getPrincipal()).isEqualTo(this.registeredClient.getClientId()); - assertThat(updatedAuthorization.getAccessToken()).isNotNull(); - assertThat(accessTokenAuthentication.getAccessToken()).isEqualTo(updatedAuthorization.getAccessToken()); - } - - @Test - public void authenticateWhenPublicClientAndS256MethodAndValidCodeVerifierThenReturnAccessToken() { - OAuth2Authorization authorization = TestOAuth2Authorizations - .authorization(this.registeredClient, createPkceParametersS256()) - .build(); - when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE))) - .thenReturn(authorization); - when(this.jwtEncoder.encode(any(), any())).thenReturn(createJwt()); - - OAuth2AuthorizationCodeAuthenticationToken authentication = createAuthorizationCodeAuthentication( - this.registeredClient, S256_CODE_VERIFIER); - - OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = - (OAuth2AccessTokenAuthenticationToken) this.authenticationProvider.authenticate(authentication); - - ArgumentCaptor authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class); - verify(this.authorizationService).save(authorizationCaptor.capture()); - OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue(); - - OAuth2ClientAuthenticationToken clientAuthentication = (OAuth2ClientAuthenticationToken) accessTokenAuthentication.getPrincipal(); - assertThat(clientAuthentication.getPrincipal()).isEqualTo(this.registeredClient.getClientId()); - assertThat(updatedAuthorization.getAccessToken()).isNotNull(); - assertThat(accessTokenAuthentication.getAccessToken()).isEqualTo(updatedAuthorization.getAccessToken()); - } - - private static Map createPkceParametersPlain() { - Map parameters = new HashMap<>(); - parameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "plain"); - parameters.put(PkceParameterNames.CODE_CHALLENGE, PLAIN_CODE_CHALLENGE); - return parameters; - } - - private static Map createPkceParametersS256() { - Map parameters = new HashMap<>(); - parameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256"); - parameters.put(PkceParameterNames.CODE_CHALLENGE, S256_CODE_CHALLENGE); - return parameters; - } - - private static OAuth2AuthorizationCodeAuthenticationToken createAuthorizationCodeAuthentication( - RegisteredClient registeredClient, String codeVerifier) { - return new OAuth2AuthorizationCodeAuthenticationToken( - AUTHORIZATION_CODE, - registeredClient.getClientId(), - registeredClient.getRedirectUris().iterator().next(), - Collections.singletonMap(PkceParameterNames.CODE_VERIFIER, codeVerifier) - ); - } - private static Jwt createJwt() { Instant issuedAt = Instant.now(); Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationTokenTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationTokenTests.java index 5d6b3fc..9e7aff2 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationTokenTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationTokenTests.java @@ -16,7 +16,6 @@ package org.springframework.security.oauth2.server.authorization.authentication; import org.junit.Test; -import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; import java.util.Collections; @@ -35,7 +34,6 @@ public class OAuth2AuthorizationCodeAuthenticationTokenTests { private String code = "code"; private OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken( TestRegisteredClients.registeredClient().build()); - private String clientId = "clientId"; private String redirectUri = "redirectUri"; private Map additionalParameters = Collections.singletonMap("param1", "value1"); @@ -48,18 +46,11 @@ public class OAuth2AuthorizationCodeAuthenticationTokenTests { @Test public void constructorWhenClientPrincipalNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new OAuth2AuthorizationCodeAuthenticationToken(this.code, (Authentication) null, this.redirectUri, null)) + assertThatThrownBy(() -> new OAuth2AuthorizationCodeAuthenticationToken(this.code, null, this.redirectUri, null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("clientPrincipal cannot be null"); } - @Test - public void constructorWhenClientIdNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new OAuth2AuthorizationCodeAuthenticationToken(this.code, (String) null, this.redirectUri, null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("clientId cannot be empty"); - } - @Test public void constructorWhenClientPrincipalProvidedThenCreated() { OAuth2AuthorizationCodeAuthenticationToken authentication = new OAuth2AuthorizationCodeAuthenticationToken( @@ -71,21 +62,10 @@ public class OAuth2AuthorizationCodeAuthenticationTokenTests { assertThat(authentication.getAdditionalParameters()).isEqualTo(this.additionalParameters); } - @Test - public void constructorWhenClientIdProvidedThenCreated() { - OAuth2AuthorizationCodeAuthenticationToken authentication = new OAuth2AuthorizationCodeAuthenticationToken( - this.code, this.clientId, this.redirectUri, this.additionalParameters); - assertThat(authentication.getPrincipal()).isEqualTo(this.clientId); - assertThat(authentication.getCredentials().toString()).isEmpty(); - assertThat(authentication.getCode()).isEqualTo(this.code); - assertThat(authentication.getRedirectUri()).isEqualTo(this.redirectUri); - assertThat(authentication.getAdditionalParameters()).isEqualTo(this.additionalParameters); - } - @Test public void getAdditionalParametersWhenUpdateThenThrowUnsupportedOperationException() { OAuth2AuthorizationCodeAuthenticationToken authentication = new OAuth2AuthorizationCodeAuthenticationToken( - this.code, this.clientId, this.redirectUri, this.additionalParameters); + this.code, this.clientPrincipal, this.redirectUri, this.additionalParameters); assertThatThrownBy(() -> authentication.getAdditionalParameters().put("another_key", 1)) .isInstanceOf(UnsupportedOperationException.class); assertThatThrownBy(() -> authentication.getAdditionalParameters().remove("some_key")) @@ -93,12 +73,4 @@ public class OAuth2AuthorizationCodeAuthenticationTokenTests { assertThatThrownBy(() -> authentication.getAdditionalParameters().clear()) .isInstanceOf(UnsupportedOperationException.class); } - - @Test - public void getClientIdWhenClientPrincipalProvidedThenNotNull() { - OAuth2AuthorizationCodeAuthenticationToken authentication = new OAuth2AuthorizationCodeAuthenticationToken( - this.code, this.clientPrincipal, this.redirectUri, this.additionalParameters); - assertThat(authentication.getClientId()).isNotNull(); - assertThat(authentication.getClientId()).isEqualTo(this.clientPrincipal.getRegisteredClient().getClientId()); - } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProviderTests.java index 03c39dd..95f4d18 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProviderTests.java @@ -17,41 +17,74 @@ package org.springframework.security.oauth2.server.authorization.authentication; import org.junit.Before; import org.junit.Test; +import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; -import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations; +import org.springframework.security.oauth2.server.authorization.TokenType; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; +import org.springframework.security.oauth2.server.authorization.config.ClientSettings; + +import java.util.HashMap; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; /** * Tests for {@link OAuth2ClientAuthenticationProvider}. * * @author Patryk Kostrzewa * @author Joe Grandja + * @author Daniel Garnier-Moiroux */ public class OAuth2ClientAuthenticationProviderTests { - private RegisteredClient registeredClient; + private static final String PLAIN_CODE_VERIFIER = "pkce-key"; + private static final String PLAIN_CODE_CHALLENGE = PLAIN_CODE_VERIFIER; + + // See RFC 7636: Appendix B. Example for the S256 code_challenge_method + // https://tools.ietf.org/html/rfc7636#appendix-B + private static final String S256_CODE_VERIFIER = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"; + private static final String S256_CODE_CHALLENGE = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"; + + private static final String AUTHORIZATION_CODE = "code"; + private RegisteredClientRepository registeredClientRepository; + private OAuth2AuthorizationService authorizationService; private OAuth2ClientAuthenticationProvider authenticationProvider; @Before public void setUp() { - this.registeredClient = TestRegisteredClients.registeredClient().build(); - this.registeredClientRepository = new InMemoryRegisteredClientRepository(this.registeredClient); - this.authenticationProvider = new OAuth2ClientAuthenticationProvider(this.registeredClientRepository); + this.registeredClientRepository = mock(RegisteredClientRepository.class); + this.authorizationService = mock(OAuth2AuthorizationService.class); + this.authenticationProvider = new OAuth2ClientAuthenticationProvider( + this.registeredClientRepository, this.authorizationService); } @Test public void constructorWhenRegisteredClientRepositoryNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new OAuth2ClientAuthenticationProvider(null)) + assertThatThrownBy(() -> new OAuth2ClientAuthenticationProvider(null, this.authorizationService)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("registeredClientRepository cannot be null"); } + @Test + public void constructorWhenAuthorizationServiceNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OAuth2ClientAuthenticationProvider(this.registeredClientRepository, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("authorizationService cannot be null"); + } + @Test public void supportsWhenTypeOAuth2ClientAuthenticationTokenThenReturnTrue() { assertThat(this.authenticationProvider.supports(OAuth2ClientAuthenticationToken.class)).isTrue(); @@ -59,8 +92,12 @@ public class OAuth2ClientAuthenticationProviderTests { @Test public void authenticateWhenInvalidClientIdThenThrowOAuth2AuthenticationException() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) + .thenReturn(registeredClient); + OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken( - this.registeredClient.getClientId() + "-invalid", this.registeredClient.getClientSecret()); + registeredClient.getClientId() + "-invalid", registeredClient.getClientSecret(), null); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) .isInstanceOf(OAuth2AuthenticationException.class) .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) @@ -70,8 +107,12 @@ public class OAuth2ClientAuthenticationProviderTests { @Test public void authenticateWhenInvalidClientSecretThenThrowOAuth2AuthenticationException() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) + .thenReturn(registeredClient); + OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken( - this.registeredClient.getClientId(), this.registeredClient.getClientSecret() + "-invalid"); + registeredClient.getClientId(), registeredClient.getClientSecret() + "-invalid", null); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) .isInstanceOf(OAuth2AuthenticationException.class) .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) @@ -81,13 +122,283 @@ public class OAuth2ClientAuthenticationProviderTests { @Test public void authenticateWhenValidCredentialsThenAuthenticated() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) + .thenReturn(registeredClient); + OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken( - this.registeredClient.getClientId(), this.registeredClient.getClientSecret()); + registeredClient.getClientId(), registeredClient.getClientSecret(), null); OAuth2ClientAuthenticationToken authenticationResult = (OAuth2ClientAuthenticationToken) this.authenticationProvider.authenticate(authentication); assertThat(authenticationResult.isAuthenticated()).isTrue(); - assertThat(authenticationResult.getPrincipal().toString()).isEqualTo(this.registeredClient.getClientId()); + assertThat(authenticationResult.getPrincipal().toString()).isEqualTo(registeredClient.getClientId()); assertThat(authenticationResult.getCredentials()).isNull(); - assertThat(authenticationResult.getRegisteredClient()).isEqualTo(this.registeredClient); + assertThat(authenticationResult.getRegisteredClient()).isEqualTo(registeredClient); + } + + @Test + public void authenticateWhenNotPkceThenContinueAuthenticated() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) + .thenReturn(registeredClient); + + OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken( + registeredClient.getClientId(), registeredClient.getClientSecret(), null); + OAuth2ClientAuthenticationToken authenticationResult = + (OAuth2ClientAuthenticationToken) this.authenticationProvider.authenticate(authentication); + assertThat(authenticationResult.isAuthenticated()).isTrue(); + + verifyNoInteractions(this.authorizationService); + } + + @Test + public void authenticateWhenPkceAndInvalidCodeThenThrowOAuth2AuthenticationException() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) + .thenReturn(registeredClient); + + OAuth2Authorization authorization = TestOAuth2Authorizations + .authorization(registeredClient, createPkceAuthorizationParametersPlain()) + .build(); + when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE))) + .thenReturn(authorization); + + Map parameters = createPkceTokenParameters(PLAIN_CODE_VERIFIER); + parameters.put(OAuth2ParameterNames.CODE, "invalid-code"); + + OAuth2ClientAuthenticationToken authentication = + new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), parameters); + + assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) + .isInstanceOf(OAuth2AuthenticationException.class) + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) + .extracting("errorCode") + .isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT); + } + + @Test + public void authenticateWhenPkceAndRequireProofKeyAndMissingCodeChallengeThenThrowOAuth2AuthenticationException() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient() + .clientSettings( + new ClientSettings().requireProofKey(true)) + .build(); + when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) + .thenReturn(registeredClient); + + OAuth2Authorization authorization = TestOAuth2Authorizations + .authorization(registeredClient) + .build(); + when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE))) + .thenReturn(authorization); + + Map parameters = createPkceTokenParameters(PLAIN_CODE_VERIFIER); + + OAuth2ClientAuthenticationToken authentication = + new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), parameters); + + assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) + .isInstanceOf(OAuth2AuthenticationException.class) + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) + .extracting("errorCode") + .isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT); + } + + @Test + public void authenticateWhenPkceAndMissingCodeVerifierThenThrowOAuth2AuthenticationException() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) + .thenReturn(registeredClient); + + OAuth2Authorization authorization = TestOAuth2Authorizations + .authorization(registeredClient, createPkceAuthorizationParametersPlain()) + .build(); + when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE))) + .thenReturn(authorization); + + Map parameters = createPkceTokenParameters(PLAIN_CODE_VERIFIER); + parameters.remove(PkceParameterNames.CODE_VERIFIER); + + OAuth2ClientAuthenticationToken authentication = + new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), parameters); + + assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) + .isInstanceOf(OAuth2AuthenticationException.class) + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) + .extracting("errorCode") + .isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT); + } + + @Test + public void authenticateWhenPkceAndPlainMethodAndInvalidCodeVerifierThenThrowOAuth2AuthenticationException() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) + .thenReturn(registeredClient); + + OAuth2Authorization authorization = TestOAuth2Authorizations + .authorization(registeredClient, createPkceAuthorizationParametersPlain()) + .build(); + when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE))) + .thenReturn(authorization); + + Map parameters = createPkceTokenParameters("invalid-code-verifier"); + + OAuth2ClientAuthenticationToken authentication = + new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), parameters); + + assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) + .isInstanceOf(OAuth2AuthenticationException.class) + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) + .extracting("errorCode") + .isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT); + } + + @Test + public void authenticateWhenPkceAndS256MethodAndInvalidCodeVerifierThenThrowOAuth2AuthenticationException() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) + .thenReturn(registeredClient); + + OAuth2Authorization authorization = TestOAuth2Authorizations + .authorization(registeredClient, createPkceAuthorizationParametersS256()) + .build(); + when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE))) + .thenReturn(authorization); + + Map parameters = createPkceTokenParameters("invalid-code-verifier"); + + OAuth2ClientAuthenticationToken authentication = + new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), parameters); + + assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) + .isInstanceOf(OAuth2AuthenticationException.class) + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) + .extracting("errorCode") + .isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT); + } + + @Test + public void authenticateWhenPkceAndPlainMethodAndValidCodeVerifierThenAuthenticated() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) + .thenReturn(registeredClient); + + OAuth2Authorization authorization = TestOAuth2Authorizations + .authorization(registeredClient, createPkceAuthorizationParametersPlain()) + .build(); + when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE))) + .thenReturn(authorization); + + Map parameters = createPkceTokenParameters(PLAIN_CODE_VERIFIER); + + OAuth2ClientAuthenticationToken authentication = + new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), parameters); + + OAuth2ClientAuthenticationToken authenticationResult = + (OAuth2ClientAuthenticationToken) this.authenticationProvider.authenticate(authentication); + assertThat(authenticationResult.isAuthenticated()).isTrue(); + assertThat(authenticationResult.getPrincipal().toString()).isEqualTo(registeredClient.getClientId()); + assertThat(authenticationResult.getCredentials()).isNull(); + assertThat(authenticationResult.getRegisteredClient()).isEqualTo(registeredClient); + } + + @Test + public void authenticateWhenPkceAndMissingMethodThenDefaultPlainMethodAndAuthenticated() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) + .thenReturn(registeredClient); + + Map authorizationRequestAdditionalParameters = createPkceAuthorizationParametersPlain(); + authorizationRequestAdditionalParameters.remove(PkceParameterNames.CODE_CHALLENGE_METHOD); + OAuth2Authorization authorization = TestOAuth2Authorizations + .authorization(registeredClient, authorizationRequestAdditionalParameters) + .build(); + when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE))) + .thenReturn(authorization); + + Map parameters = createPkceTokenParameters(PLAIN_CODE_VERIFIER); + + OAuth2ClientAuthenticationToken authentication = + new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), parameters); + + OAuth2ClientAuthenticationToken authenticationResult = + (OAuth2ClientAuthenticationToken) this.authenticationProvider.authenticate(authentication); + assertThat(authenticationResult.isAuthenticated()).isTrue(); + assertThat(authenticationResult.getPrincipal().toString()).isEqualTo(registeredClient.getClientId()); + assertThat(authenticationResult.getCredentials()).isNull(); + assertThat(authenticationResult.getRegisteredClient()).isEqualTo(registeredClient); + } + + @Test + public void authenticateWhenPkceAndS256MethodAndValidCodeVerifierThenAuthenticated() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) + .thenReturn(registeredClient); + + OAuth2Authorization authorization = TestOAuth2Authorizations + .authorization(registeredClient, createPkceAuthorizationParametersS256()) + .build(); + when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE))) + .thenReturn(authorization); + + Map parameters = createPkceTokenParameters(S256_CODE_VERIFIER); + + OAuth2ClientAuthenticationToken authentication = + new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), parameters); + + OAuth2ClientAuthenticationToken authenticationResult = + (OAuth2ClientAuthenticationToken) this.authenticationProvider.authenticate(authentication); + assertThat(authenticationResult.isAuthenticated()).isTrue(); + assertThat(authenticationResult.getPrincipal().toString()).isEqualTo(registeredClient.getClientId()); + assertThat(authenticationResult.getCredentials()).isNull(); + assertThat(authenticationResult.getRegisteredClient()).isEqualTo(registeredClient); + } + + @Test + public void authenticateWhenPkceAndUnsupportedCodeChallengeMethodThenThrowOAuth2AuthenticationException() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) + .thenReturn(registeredClient); + + Map authorizationRequestAdditionalParameters = createPkceAuthorizationParametersPlain(); + // This should never happen: the Authorization endpoint should not allow it + authorizationRequestAdditionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "unsupported-challenge-method"); + OAuth2Authorization authorization = TestOAuth2Authorizations + .authorization(registeredClient, authorizationRequestAdditionalParameters) + .build(); + when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE))) + .thenReturn(authorization); + + Map parameters = createPkceTokenParameters(PLAIN_CODE_VERIFIER); + + OAuth2ClientAuthenticationToken authentication = + new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), parameters); + + assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) + .isInstanceOf(OAuth2AuthenticationException.class) + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) + .extracting("errorCode") + .isEqualTo(OAuth2ErrorCodes.SERVER_ERROR); + } + + private static Map createPkceTokenParameters(String codeVerifier) { + Map parameters = new HashMap<>(); + parameters.put(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue()); + parameters.put(OAuth2ParameterNames.CODE, AUTHORIZATION_CODE); + parameters.put(PkceParameterNames.CODE_VERIFIER, codeVerifier); + return parameters; + } + + private static Map createPkceAuthorizationParametersPlain() { + Map parameters = new HashMap<>(); + parameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "plain"); + parameters.put(PkceParameterNames.CODE_CHALLENGE, PLAIN_CODE_CHALLENGE); + return parameters; + } + + private static Map createPkceAuthorizationParametersS256() { + Map parameters = new HashMap<>(); + parameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256"); + parameters.put(PkceParameterNames.CODE_CHALLENGE, S256_CODE_CHALLENGE); + return parameters; } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationTokenTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationTokenTests.java index 2147626..f4ba363 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationTokenTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationTokenTests.java @@ -19,6 +19,9 @@ import org.junit.Test; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; +import java.util.HashMap; +import java.util.Map; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -31,14 +34,14 @@ public class OAuth2ClientAuthenticationTokenTests { @Test public void constructorWhenClientIdNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new OAuth2ClientAuthenticationToken(null, "secret")) + assertThatThrownBy(() -> new OAuth2ClientAuthenticationToken(null, "secret", null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("clientId cannot be empty"); } @Test public void constructorWhenClientSecretNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> new OAuth2ClientAuthenticationToken("clientId", null)) + assertThatThrownBy(() -> new OAuth2ClientAuthenticationToken("clientId", null, null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("clientSecret cannot be empty"); } @@ -52,13 +55,25 @@ public class OAuth2ClientAuthenticationTokenTests { @Test public void constructorWhenClientCredentialsProvidedThenCreated() { - OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken("clientId", "secret"); + OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken("clientId", "secret", null); assertThat(authentication.isAuthenticated()).isFalse(); assertThat(authentication.getPrincipal().toString()).isEqualTo("clientId"); assertThat(authentication.getCredentials()).isEqualTo("secret"); assertThat(authentication.getRegisteredClient()).isNull(); } + @Test + public void constructorWhenClientIdProvidedThenCreated() { + Map additionalParameters = new HashMap<>(); + additionalParameters.put("param1", "value1"); + OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken("clientId", additionalParameters); + assertThat(authentication.isAuthenticated()).isFalse(); + assertThat(authentication.getPrincipal().toString()).isEqualTo("clientId"); + assertThat(authentication.getCredentials()).isNull(); + assertThat(authentication.getAdditionalParameters()).isEqualTo(additionalParameters); + assertThat(authentication.getRegisteredClient()).isNull(); + } + @Test public void constructorWhenRegisteredClientProvidedThenCreated() { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java index b1e6368..3957b63 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java @@ -103,7 +103,7 @@ public class OAuth2ClientCredentialsAuthenticationProviderTests { @Test public void authenticateWhenClientPrincipalNotAuthenticatedThenThrowOAuth2AuthenticationException() { OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken( - this.registeredClient.getClientId(), this.registeredClient.getClientSecret()); + this.registeredClient.getClientId(), this.registeredClient.getClientSecret(), null); OAuth2ClientCredentialsAuthenticationToken authentication = new OAuth2ClientCredentialsAuthenticationToken(clientPrincipal); assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/ClientSecretBasicAuthenticationConverterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/ClientSecretBasicAuthenticationConverterTests.java index 39e521e..a91647b 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/ClientSecretBasicAuthenticationConverterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/ClientSecretBasicAuthenticationConverterTests.java @@ -19,8 +19,11 @@ import org.junit.Test; import org.springframework.http.HttpHeaders; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; import java.net.URLEncoder; @@ -29,6 +32,7 @@ import java.util.Base64; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.entry; /** * Tests for {@link ClientSecretBasicAuthenticationConverter}. @@ -96,6 +100,20 @@ public class ClientSecretBasicAuthenticationConverterTests { assertThat(authentication.getCredentials()).isEqualTo("secret"); } + @Test + public void convertWhenConfidentialClientWithPkceParametersThenAdditionalParametersIncluded() throws Exception { + MockHttpServletRequest request = createPkceTokenRequest(); + request.addHeader(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth("clientId", "secret")); + OAuth2ClientAuthenticationToken authentication = (OAuth2ClientAuthenticationToken) this.converter.convert(request); + assertThat(authentication.getPrincipal()).isEqualTo("clientId"); + assertThat(authentication.getCredentials()).isEqualTo("secret"); + assertThat(authentication.getAdditionalParameters()) + .containsOnly( + entry(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue()), + entry(OAuth2ParameterNames.CODE, "code"), + entry(PkceParameterNames.CODE_VERIFIER, "code-verifier-1")); + } + private static String encodeBasicAuth(String clientId, String secret) throws Exception { clientId = URLEncoder.encode(clientId, StandardCharsets.UTF_8.name()); secret = URLEncoder.encode(secret, StandardCharsets.UTF_8.name()); @@ -103,4 +121,12 @@ public class ClientSecretBasicAuthenticationConverterTests { byte[] encodedBytes = Base64.getEncoder().encode(credentialsString.getBytes(StandardCharsets.UTF_8)); return new String(encodedBytes, StandardCharsets.UTF_8); } + + private static MockHttpServletRequest createPkceTokenRequest() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue()); + request.addParameter(OAuth2ParameterNames.CODE, "code"); + request.addParameter(PkceParameterNames.CODE_VERIFIER, "code-verifier-1"); + return request; + } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilterTests.java index 3d8d6fb..7867f31 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilterTests.java @@ -162,7 +162,7 @@ public class OAuth2ClientAuthenticationFilterTests { @Test public void doFilterWhenRequestMatchesAndBadCredentialsThenInvalidClientError() throws Exception { when(this.authenticationConverter.convert(any(HttpServletRequest.class))).thenReturn( - new OAuth2ClientAuthenticationToken("clientId", "invalid-secret")); + new OAuth2ClientAuthenticationToken("clientId", "invalid-secret", null)); when(this.authenticationManager.authenticate(any(Authentication.class))).thenThrow( new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT))); @@ -185,7 +185,7 @@ public class OAuth2ClientAuthenticationFilterTests { public void doFilterWhenRequestMatchesAndValidCredentialsThenProcessed() throws Exception { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); when(this.authenticationConverter.convert(any(HttpServletRequest.class))).thenReturn( - new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), registeredClient.getClientSecret())); + new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), registeredClient.getClientSecret(), null)); when(this.authenticationManager.authenticate(any(Authentication.class))).thenReturn( new OAuth2ClientAuthenticationToken(registeredClient)); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilterTests.java index 409816d..1a8c26e 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilterTests.java @@ -34,7 +34,6 @@ import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; -import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; @@ -167,17 +166,6 @@ public class OAuth2TokenEndpointFilterTests { OAuth2ParameterNames.GRANT_TYPE, OAuth2ErrorCodes.UNSUPPORTED_GRANT_TYPE, request); } - @Test - public void doFilterWhenTokenRequestMultipleClientIdThenInvalidRequestError() throws Exception { - MockHttpServletRequest request = createAuthorizationCodeTokenRequest( - TestRegisteredClients.registeredClient().build()); - request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-1"); - request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-2"); - - doFilterWhenTokenRequestInvalidParameterThenError( - OAuth2ParameterNames.CLIENT_ID, OAuth2ErrorCodes.INVALID_REQUEST, request); - } - @Test public void doFilterWhenTokenRequestMissingCodeThenInvalidRequestError() throws Exception { MockHttpServletRequest request = createAuthorizationCodeTokenRequest( @@ -208,26 +196,6 @@ public class OAuth2TokenEndpointFilterTests { OAuth2ParameterNames.REDIRECT_URI, OAuth2ErrorCodes.INVALID_REQUEST, request); } - @Test - public void doFilterWhenTokenRequestNotAuthenticatedAndMissingCodeVerifierThenInvalidRequestError() throws Exception { - MockHttpServletRequest request = createAuthorizationCodeTokenRequest( - TestRegisteredClients.registeredClient().build()); - - doFilterWhenTokenRequestInvalidParameterThenError( - PkceParameterNames.CODE_VERIFIER, OAuth2ErrorCodes.INVALID_REQUEST, request); - } - - @Test - public void doFilterWhenTokenRequestNotAuthenticatedAndMultipleCodeVerifierThenInvalidRequestError() throws Exception { - MockHttpServletRequest request = createAuthorizationCodeTokenRequest( - TestRegisteredClients.registeredClient().build()); - request.addParameter(PkceParameterNames.CODE_VERIFIER, "one-verifier"); - request.addParameter(PkceParameterNames.CODE_VERIFIER, "two-verifier2"); - - doFilterWhenTokenRequestInvalidParameterThenError( - PkceParameterNames.CODE_VERIFIER, OAuth2ErrorCodes.INVALID_REQUEST, request); - } - @Test public void doFilterWhenAuthorizationCodeTokenRequestValidThenAccessTokenResponse() throws Exception { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/PublicClientAuthenticationConverterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/PublicClientAuthenticationConverterTests.java new file mode 100644 index 0000000..410664d --- /dev/null +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/PublicClientAuthenticationConverterTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.server.authorization.web; + +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link PublicClientAuthenticationConverter}. + * + * @author Joe Grandja + */ +public class PublicClientAuthenticationConverterTests { + private PublicClientAuthenticationConverter converter = new PublicClientAuthenticationConverter(); + + @Test + public void convertWhenNotPublicClientThenReturnNull() { + MockHttpServletRequest request = new MockHttpServletRequest(); + Authentication authentication = this.converter.convert(request); + assertThat(authentication).isNull(); + } + + @Test + public void convertWhenMissingClientIdThenReturnNull() { + MockHttpServletRequest request = createPkceTokenRequest(); + request.removeParameter(OAuth2ParameterNames.CLIENT_ID); + Authentication authentication = this.converter.convert(request); + assertThat(authentication).isNull(); + } + + @Test + public void convertWhenMultipleClientIdThenInvalidRequestError() { + MockHttpServletRequest request = createPkceTokenRequest(); + request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-2"); + assertThatThrownBy(() -> this.converter.convert(request)) + .isInstanceOf(OAuth2AuthenticationException.class) + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) + .extracting("errorCode") + .isEqualTo(OAuth2ErrorCodes.INVALID_REQUEST); + } + + @Test + public void convertWhenMultipleCodeVerifierThenInvalidRequestError() { + MockHttpServletRequest request = createPkceTokenRequest(); + request.addParameter(PkceParameterNames.CODE_VERIFIER, "code-verifier-2"); + assertThatThrownBy(() -> this.converter.convert(request)) + .isInstanceOf(OAuth2AuthenticationException.class) + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) + .extracting("errorCode") + .isEqualTo(OAuth2ErrorCodes.INVALID_REQUEST); + } + + @Test + public void convertWhenPublicClientThenReturnClientAuthenticationToken() { + MockHttpServletRequest request = createPkceTokenRequest(); + OAuth2ClientAuthenticationToken authentication = (OAuth2ClientAuthenticationToken) this.converter.convert(request); + assertThat(authentication.getPrincipal()).isEqualTo("client-1"); + assertThat(authentication.getAdditionalParameters()) + .containsOnly( + entry(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue()), + entry(OAuth2ParameterNames.CODE, "code"), + entry(PkceParameterNames.CODE_VERIFIER, "code-verifier-1")); + } + + private static MockHttpServletRequest createPkceTokenRequest() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue()); + request.addParameter(OAuth2ParameterNames.CODE, "code"); + request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-1"); + request.addParameter(PkceParameterNames.CODE_VERIFIER, "code-verifier-1"); + return request; + } +}