From 543fa264b3825c7c7c74377622d8be1eb13b0a20 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Tue, 6 Jul 2021 14:39:57 -0400 Subject: [PATCH] Provide extension for processing authorization request Issue gh-342 --- .../OAuth2AuthorizationEndpointFilter.java | 175 +++-------------- ...ionCodeRequestAuthenticationConverter.java | 185 ++++++++++++++++++ ...Auth2AuthorizationEndpointFilterTests.java | 33 ++++ 3 files changed, 242 insertions(+), 151 deletions(-) create mode 100644 oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2AuthorizationCodeRequestAuthenticationConverter.java diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilter.java index ca37eb82..ea2e017e 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilter.java @@ -17,11 +17,8 @@ package org.springframework.security.oauth2.server.authorization.web; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.Arrays; import java.util.HashSet; -import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; import javax.servlet.FilterChain; import javax.servlet.ServletException; @@ -31,24 +28,20 @@ import javax.servlet.http.HttpServletResponse; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolder; 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.OAuth2AuthorizationResponseType; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; -import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; import org.springframework.security.oauth2.core.oidc.OidcScopes; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCodeRequestAuthenticationException; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter; import org.springframework.security.web.DefaultRedirectStrategy; import org.springframework.security.web.RedirectStrategy; import org.springframework.security.web.authentication.AuthenticationConverter; @@ -60,7 +53,6 @@ import org.springframework.security.web.util.matcher.NegatedRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; -import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.util.UriComponentsBuilder; @@ -88,8 +80,8 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter { private final AuthenticationManager authenticationManager; private final RequestMatcher authorizationEndpointMatcher; - private final AuthenticationConverter authenticationConverter; private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); + private AuthenticationConverter authenticationConverter; private String consentPage; /** @@ -164,17 +156,6 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter { return new OrRequestMatcher(authorizationRequestMatcher, authorizationConsentMatcher); } - /** - * Specify the URI to redirect Resource Owners to if consent is required. A default consent - * page will be generated when this attribute is not specified. - * - * @param consentPage the URI of the custom consent page to redirect to if consent is required (e.g. "/oauth2/consent") - * @see org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer#consentPage(String) - */ - public final void setConsentPage(String consentPage) { - this.consentPage = consentPage; - } - @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { @@ -212,6 +193,28 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter { } } + /** + * Sets the {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest} + * to an instance of {@link OAuth2AuthorizationCodeRequestAuthenticationToken} used for authenticating the request. + * + * @param authenticationConverter the {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest} + */ + public final void setAuthenticationConverter(AuthenticationConverter authenticationConverter) { + Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); + this.authenticationConverter = authenticationConverter; + } + + /** + * Specify the URI to redirect Resource Owners to if consent is required. A default consent + * page will be generated when this attribute is not specified. + * + * @param consentPage the URI of the custom consent page to redirect to if consent is required (e.g. "/oauth2/consent") + * @see org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer#consentPage(String) + */ + public final void setConsentPage(String consentPage) { + this.consentPage = consentPage; + } + private void sendAuthorizationConsent(HttpServletRequest request, HttpServletResponse response, OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication, OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult) throws IOException { @@ -294,136 +297,6 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter { this.redirectStrategy.sendRedirect(request, response, uriBuilder.toUriString()); } - private static class OAuth2AuthorizationCodeRequestAuthenticationConverter implements AuthenticationConverter { - private static final Authentication ANONYMOUS_AUTHENTICATION = new AnonymousAuthenticationToken( - "anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); - private static final String PKCE_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1"; - private final RequestMatcher oidcAuthenticationRequestMatcher; - - private OAuth2AuthorizationCodeRequestAuthenticationConverter() { - RequestMatcher postMethodMatcher = request -> "POST".equals(request.getMethod()); - RequestMatcher responseTypeParameterMatcher = request -> - request.getParameter(OAuth2ParameterNames.RESPONSE_TYPE) != null; - RequestMatcher openidScopeMatcher = request -> { - String scope = request.getParameter(OAuth2ParameterNames.SCOPE); - return StringUtils.hasText(scope) && scope.contains(OidcScopes.OPENID); - }; - this.oidcAuthenticationRequestMatcher = new AndRequestMatcher( - postMethodMatcher, responseTypeParameterMatcher, openidScopeMatcher); - } - - @Override - public Authentication convert(HttpServletRequest request) { - MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request); - - boolean authorizationRequest = false; - if ("GET".equals(request.getMethod()) || this.oidcAuthenticationRequestMatcher.matches(request)) { - authorizationRequest = true; - - // response_type (REQUIRED) - String responseType = request.getParameter(OAuth2ParameterNames.RESPONSE_TYPE); - if (!StringUtils.hasText(responseType) || - parameters.get(OAuth2ParameterNames.RESPONSE_TYPE).size() != 1) { - throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.RESPONSE_TYPE); - } else if (!responseType.equals(OAuth2AuthorizationResponseType.CODE.getValue())) { - throwError(OAuth2ErrorCodes.UNSUPPORTED_RESPONSE_TYPE, OAuth2ParameterNames.RESPONSE_TYPE); - } - } - - String authorizationUri = request.getRequestURL().toString(); - - // client_id (REQUIRED) - String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID); - if (!StringUtils.hasText(clientId) || - parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) { - throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID); - } - - Authentication principal = SecurityContextHolder.getContext().getAuthentication(); - if (principal == null) { - principal = ANONYMOUS_AUTHENTICATION; - } - - // redirect_uri (OPTIONAL) - String redirectUri = parameters.getFirst(OAuth2ParameterNames.REDIRECT_URI); - if (StringUtils.hasText(redirectUri) && - parameters.get(OAuth2ParameterNames.REDIRECT_URI).size() != 1) { - throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI); - } - - // scope (OPTIONAL) - Set scopes = null; - if (authorizationRequest) { - String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE); - if (StringUtils.hasText(scope) && - parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) { - throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE); - } - if (StringUtils.hasText(scope)) { - scopes = new HashSet<>( - Arrays.asList(StringUtils.delimitedListToStringArray(scope, " "))); - } - } else { - // Consent request - if (parameters.containsKey(OAuth2ParameterNames.SCOPE)) { - scopes = new HashSet<>(parameters.get(OAuth2ParameterNames.SCOPE)); - } - } - - // state (RECOMMENDED) - String state = parameters.getFirst(OAuth2ParameterNames.STATE); - if (StringUtils.hasText(state) && - parameters.get(OAuth2ParameterNames.STATE).size() != 1) { - throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE); - } - - // code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE) - String codeChallenge = parameters.getFirst(PkceParameterNames.CODE_CHALLENGE); - if (StringUtils.hasText(codeChallenge) && - parameters.get(PkceParameterNames.CODE_CHALLENGE).size() != 1) { - throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE, PKCE_ERROR_URI); - } - - // code_challenge_method (OPTIONAL for public clients) - RFC 7636 (PKCE) - String codeChallengeMethod = parameters.getFirst(PkceParameterNames.CODE_CHALLENGE_METHOD); - if (StringUtils.hasText(codeChallengeMethod) && - parameters.get(PkceParameterNames.CODE_CHALLENGE_METHOD).size() != 1) { - throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, PKCE_ERROR_URI); - } - - // @formatter:off - Map additionalParameters = parameters - .entrySet() - .stream() - .filter(e -> !e.getKey().equals(OAuth2ParameterNames.RESPONSE_TYPE) && - !e.getKey().equals(OAuth2ParameterNames.CLIENT_ID) && - !e.getKey().equals(OAuth2ParameterNames.REDIRECT_URI) && - !e.getKey().equals(OAuth2ParameterNames.SCOPE) && - !e.getKey().equals(OAuth2ParameterNames.STATE)) - .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0))); - // @formatter:on - - return OAuth2AuthorizationCodeRequestAuthenticationToken.with(clientId, principal) - .authorizationUri(authorizationUri) - .redirectUri(redirectUri) - .scopes(scopes) - .state(state) - .additionalParameters(additionalParameters) - .consent(!authorizationRequest) - .build(); - } - - private static void throwError(String errorCode, String parameterName) { - throwError(errorCode, parameterName, "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1"); - } - - private static void throwError(String errorCode, String parameterName, String errorUri) { - OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri); - throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null); - } - - } - /** * For internal use only. */ diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2AuthorizationCodeRequestAuthenticationConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2AuthorizationCodeRequestAuthenticationConverter.java new file mode 100644 index 00000000..1464a45f --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2AuthorizationCodeRequestAuthenticationConverter.java @@ -0,0 +1,185 @@ +/* + * Copyright 2020-2021 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.authentication; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; +import org.springframework.security.oauth2.core.oidc.OidcScopes; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCodeRequestAuthenticationException; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter; +import org.springframework.security.web.authentication.AuthenticationConverter; +import org.springframework.security.web.util.matcher.AndRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; + +/** + * Attempts to extract an Authorization Request (or Consent) from {@link HttpServletRequest} + * for the OAuth 2.0 Authorization Code Grant and then converts it to + * an {@link OAuth2AuthorizationCodeRequestAuthenticationToken} used for authenticating the request. + * + * @author Joe Grandja + * @since 0.1.2 + * @see AuthenticationConverter + * @see OAuth2AuthorizationCodeRequestAuthenticationToken + * @see OAuth2AuthorizationEndpointFilter + */ +public final class OAuth2AuthorizationCodeRequestAuthenticationConverter implements AuthenticationConverter { + private static final String DEFAULT_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1"; + private static final String PKCE_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1"; + private static final Authentication ANONYMOUS_AUTHENTICATION = new AnonymousAuthenticationToken( + "anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); + private static final RequestMatcher OIDC_REQUEST_MATCHER = createOidcRequestMatcher(); + + @Override + public Authentication convert(HttpServletRequest request) { + MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request); + + boolean authorizationRequest = false; + if ("GET".equals(request.getMethod()) || OIDC_REQUEST_MATCHER.matches(request)) { + authorizationRequest = true; + + // response_type (REQUIRED) + String responseType = request.getParameter(OAuth2ParameterNames.RESPONSE_TYPE); + if (!StringUtils.hasText(responseType) || + parameters.get(OAuth2ParameterNames.RESPONSE_TYPE).size() != 1) { + throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.RESPONSE_TYPE); + } else if (!responseType.equals(OAuth2AuthorizationResponseType.CODE.getValue())) { + throwError(OAuth2ErrorCodes.UNSUPPORTED_RESPONSE_TYPE, OAuth2ParameterNames.RESPONSE_TYPE); + } + } + + String authorizationUri = request.getRequestURL().toString(); + + // client_id (REQUIRED) + String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID); + if (!StringUtils.hasText(clientId) || + parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) { + throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID); + } + + Authentication principal = SecurityContextHolder.getContext().getAuthentication(); + if (principal == null) { + principal = ANONYMOUS_AUTHENTICATION; + } + + // redirect_uri (OPTIONAL) + String redirectUri = parameters.getFirst(OAuth2ParameterNames.REDIRECT_URI); + if (StringUtils.hasText(redirectUri) && + parameters.get(OAuth2ParameterNames.REDIRECT_URI).size() != 1) { + throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI); + } + + // scope (OPTIONAL) + Set scopes = null; + if (authorizationRequest) { + String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE); + if (StringUtils.hasText(scope) && + parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) { + throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE); + } + if (StringUtils.hasText(scope)) { + scopes = new HashSet<>( + Arrays.asList(StringUtils.delimitedListToStringArray(scope, " "))); + } + } else { + // Consent request + if (parameters.containsKey(OAuth2ParameterNames.SCOPE)) { + scopes = new HashSet<>(parameters.get(OAuth2ParameterNames.SCOPE)); + } + } + + // state (RECOMMENDED) + String state = parameters.getFirst(OAuth2ParameterNames.STATE); + if (StringUtils.hasText(state) && + parameters.get(OAuth2ParameterNames.STATE).size() != 1) { + throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE); + } + + // code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE) + String codeChallenge = parameters.getFirst(PkceParameterNames.CODE_CHALLENGE); + if (StringUtils.hasText(codeChallenge) && + parameters.get(PkceParameterNames.CODE_CHALLENGE).size() != 1) { + throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE, PKCE_ERROR_URI); + } + + // code_challenge_method (OPTIONAL for public clients) - RFC 7636 (PKCE) + String codeChallengeMethod = parameters.getFirst(PkceParameterNames.CODE_CHALLENGE_METHOD); + if (StringUtils.hasText(codeChallengeMethod) && + parameters.get(PkceParameterNames.CODE_CHALLENGE_METHOD).size() != 1) { + throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, PKCE_ERROR_URI); + } + + // @formatter:off + Map additionalParameters = parameters + .entrySet() + .stream() + .filter(e -> !e.getKey().equals(OAuth2ParameterNames.RESPONSE_TYPE) && + !e.getKey().equals(OAuth2ParameterNames.CLIENT_ID) && + !e.getKey().equals(OAuth2ParameterNames.REDIRECT_URI) && + !e.getKey().equals(OAuth2ParameterNames.SCOPE) && + !e.getKey().equals(OAuth2ParameterNames.STATE)) + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0))); + // @formatter:on + + return OAuth2AuthorizationCodeRequestAuthenticationToken.with(clientId, principal) + .authorizationUri(authorizationUri) + .redirectUri(redirectUri) + .scopes(scopes) + .state(state) + .additionalParameters(additionalParameters) + .consent(!authorizationRequest) + .build(); + } + + private static RequestMatcher createOidcRequestMatcher() { + RequestMatcher postMethodMatcher = request -> "POST".equals(request.getMethod()); + RequestMatcher responseTypeParameterMatcher = request -> + request.getParameter(OAuth2ParameterNames.RESPONSE_TYPE) != null; + RequestMatcher openidScopeMatcher = request -> { + String scope = request.getParameter(OAuth2ParameterNames.SCOPE); + return StringUtils.hasText(scope) && scope.contains(OidcScopes.OPENID); + }; + return new AndRequestMatcher( + postMethodMatcher, responseTypeParameterMatcher, openidScopeMatcher); + } + + private static void throwError(String errorCode, String parameterName) { + throwError(errorCode, parameterName, DEFAULT_ERROR_URI); + } + + private static void throwError(String errorCode, String parameterName, String errorUri) { + OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri); + throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null); + } + +} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilterTests.java index 653bf5ba..cac611d2 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilterTests.java @@ -52,6 +52,7 @@ import org.springframework.security.oauth2.server.authorization.OAuth2Authorizat import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; +import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -110,6 +111,13 @@ public class OAuth2AuthorizationEndpointFilterTests { .hasMessage("authorizationEndpointUri cannot be empty"); } + @Test + public void setAuthenticationConverterWhenNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.filter.setAuthenticationConverter(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("authenticationConverter cannot be null"); + } + @Test public void doFilterWhenNotAuthorizationRequestThenNotProcessed() throws Exception { String requestUri = "/path"; @@ -242,6 +250,31 @@ public class OAuth2AuthorizationEndpointFilterTests { assertThat(response.getRedirectedUrl()).isEqualTo("https://example.com?error=errorCode&error_description=errorDescription&error_uri=errorUri&state=state"); } + @Test + public void doFilterWhenCustomAuthenticationConverterThenUsed() throws Exception { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = + authorizationCodeRequestAuthentication(registeredClient, this.principal) + .build(); + + AuthenticationConverter authenticationConverter = mock(AuthenticationConverter.class); + when(authenticationConverter.convert(any())).thenReturn(authorizationCodeRequestAuthentication); + this.filter.setAuthenticationConverter(authenticationConverter); + + when(this.authenticationManager.authenticate(any())) + .thenReturn(authorizationCodeRequestAuthentication); + + MockHttpServletRequest request = createAuthorizationRequest(registeredClient); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + this.filter.doFilter(request, response, filterChain); + + verify(authenticationConverter).convert(any()); + verify(this.authenticationManager).authenticate(any()); + verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + } + @Test public void doFilterWhenAuthorizationRequestPrincipalNotAuthenticatedThenCommenceAuthentication() throws Exception { this.principal.setAuthenticated(false);