From b9ab4929b7262e0006ee1aa58e3bf1d4fc5c673c Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 2 Aug 2018 15:35:20 -0500 Subject: [PATCH] Add OAuth2AuthorizationCodeGrantWebFilter Issue: gh-5620 --- .../config/web/server/ServerHttpSecurity.java | 107 ++++++++++- .../web/server/OAuth2ClientSpecTests.java | 121 +++++++++++++ ...OAuth2AuthorizationCodeGrantWebFilter.java | 167 ++++++++++++++++++ ...2AuthorizationCodeGrantWebFilterTests.java | 125 +++++++++++++ 4 files changed, 519 insertions(+), 1 deletion(-) create mode 100644 config/src/test/java/org/springframework/security/config/web/server/OAuth2ClientSpecTests.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/OAuth2AuthorizationCodeGrantWebFilter.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/OAuth2AuthorizationCodeGrantWebFilterTests.java diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index 78d5921163..97690e577c 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -31,6 +31,7 @@ import org.springframework.security.authorization.ReactiveAuthorizationManager; import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeReactiveAuthenticationManager; import org.springframework.security.oauth2.client.authentication.OAuth2LoginReactiveAuthenticationManager; import org.springframework.security.oauth2.client.endpoint.WebClientReactiveAuthorizationCodeTokenResponseClient; import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeReactiveAuthenticationManager; @@ -43,6 +44,7 @@ import org.springframework.security.oauth2.client.web.server.OAuth2Authorization import org.springframework.security.oauth2.client.web.server.AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationCodeAuthenticationTokenConverter; +import org.springframework.security.oauth2.client.web.server.OAuth2AuthorizationCodeGrantWebFilter; import org.springframework.security.oauth2.client.web.server.authentication.OAuth2LoginAuthenticationWebFilter; import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; @@ -575,7 +577,7 @@ public class ServerHttpSecurity { * .oauth2() * .resourceServer() * .jwt() - * .jwkSeturi(jwkSetUri); + * .jwkSetUri(jwkSetUri); * return http.build(); * } * @@ -597,6 +599,106 @@ public class ServerHttpSecurity { public class OAuth2Spec { private ResourceServerSpec resourceServer; + private OAuth2ClientSpec client; + + /** + * Configures the OAuth2 client. + * + *
+		 *  @Bean
+		 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
+		 *      http
+		 *          // ...
+		 *          .oauth2()
+		 *              .client()
+		 *                  .clientRegistrationRepository(clientRegistrationRepository)
+		 *                  .authorizedClientRepository(authorizedClientRepository);
+		 *      return http.build();
+		 *  }
+		 * 
+ * + * + * @return the {@link OAuth2ClientSpec} to customize + */ + public OAuth2ClientSpec client() { + if (this.client == null) { + this.client = new OAuth2ClientSpec(); + } + return this.client; + } + + public class OAuth2ClientSpec { + private ReactiveClientRegistrationRepository clientRegistrationRepository; + + private ServerOAuth2AuthorizedClientRepository authorizedClientRepository; + + /** + * Configures the {@link ReactiveClientRegistrationRepository}. Default is to look the value up as a Bean. + * @param clientRegistrationRepository the repository to use + * @return the {@link OAuth2ClientSpec} to customize + */ + public OAuth2ClientSpec clientRegistrationRepository(ReactiveClientRegistrationRepository clientRegistrationRepository) { + this.clientRegistrationRepository = clientRegistrationRepository; + return this; + } + + /** + * Configures the {@link ReactiveClientRegistrationRepository}. Default is to look the value up as a Bean. + * @param authorizedClientRepository the repository to use + * @return the {@link OAuth2ClientSpec} to customize + */ + public OAuth2ClientSpec authorizedClientRepository(ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { + this.authorizedClientRepository = authorizedClientRepository; + return this; + } + + protected void configure(ServerHttpSecurity http) { + ReactiveClientRegistrationRepository clientRegistrationRepository = getClientRegistrationRepository(); + ServerOAuth2AuthorizedClientRepository authorizedClientRepository = getAuthorizedClientRepository(); + ReactiveAuthenticationManager authenticationManager = new OAuth2AuthorizationCodeReactiveAuthenticationManager(new WebClientReactiveAuthorizationCodeTokenResponseClient()); + OAuth2AuthorizationCodeGrantWebFilter codeGrantWebFilter = new OAuth2AuthorizationCodeGrantWebFilter(authenticationManager, + clientRegistrationRepository, + authorizedClientRepository); + + OAuth2AuthorizationRequestRedirectWebFilter oauthRedirectFilter = new OAuth2AuthorizationRequestRedirectWebFilter( + clientRegistrationRepository); + http.addFilterAt(codeGrantWebFilter, SecurityWebFiltersOrder.AUTHENTICATION); + http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC); + } + + private ReactiveClientRegistrationRepository getClientRegistrationRepository() { + if (this.clientRegistrationRepository != null) { + return this.clientRegistrationRepository; + } + return getBeanOrNull(ReactiveClientRegistrationRepository.class); + } + + private ServerOAuth2AuthorizedClientRepository getAuthorizedClientRepository() { + if (this.authorizedClientRepository != null) { + return this.authorizedClientRepository; + } + ServerOAuth2AuthorizedClientRepository result = getBeanOrNull(ServerOAuth2AuthorizedClientRepository.class); + if (result == null) { + ReactiveOAuth2AuthorizedClientService authorizedClientService = getAuthorizedClientService(); + if (authorizedClientService != null) { + result = new AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository( + authorizedClientService); + } + } + return result; + } + + private ReactiveOAuth2AuthorizedClientService getAuthorizedClientService() { + ReactiveOAuth2AuthorizedClientService service = getBeanOrNull(ReactiveOAuth2AuthorizedClientService.class); + if (service == null) { + service = new InMemoryReactiveOAuth2AuthorizedClientService(getClientRegistrationRepository()); + } + return service; + } + + private OAuth2ClientSpec() {} + } + public ResourceServerSpec resourceServer() { if (this.resourceServer == null) { this.resourceServer = new ResourceServerSpec(); @@ -693,6 +795,9 @@ public class ServerHttpSecurity { if (this.resourceServer != null) { this.resourceServer.configure(http); } + if (this.client != null) { + this.client.configure(http); + } } private OAuth2Spec() {} diff --git a/config/src/test/java/org/springframework/security/config/web/server/OAuth2ClientSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/OAuth2ClientSpecTests.java new file mode 100644 index 0000000000..cae98a8934 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/web/server/OAuth2ClientSpecTests.java @@ -0,0 +1,121 @@ +/* + * Copyright 2002-2018 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 + * + * http://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.config.web.server; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.TestClientRegistrations; +import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository; +import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.reactive.config.EnableWebFlux; +import reactor.core.publisher.Mono; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author Rob Winch + * @since 5.1 + */ +@RunWith(SpringRunner.class) +@SecurityTestExecutionListeners +public class OAuth2ClientSpecTests { + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + private WebTestClient client; + + @Autowired + public void setApplicationContext(ApplicationContext context) { + this.client = WebTestClient.bindToApplicationContext(context).build(); + } + + @Test + @WithMockUser + public void registeredOAuth2AuthorizedClientWhenAuthenticatedThenRedirects() { + this.spring.register(Config.class, AuthorizedClientController.class).autowire(); + ReactiveClientRegistrationRepository repository = this.spring.getContext() + .getBean(ReactiveClientRegistrationRepository.class); + ServerOAuth2AuthorizedClientRepository authorizedClientRepository = this.spring.getContext().getBean(ServerOAuth2AuthorizedClientRepository.class); + when(repository.findByRegistrationId(any())).thenReturn(Mono.just(TestClientRegistrations.clientRegistration().build())); + when(authorizedClientRepository.loadAuthorizedClient(any(), any(), any())).thenReturn(Mono.empty()); + + this.client.get().uri("/") + .exchange() + .expectStatus().is3xxRedirection(); + } + + @Test + public void registeredOAuth2AuthorizedClientWhenAnonymousThenRedirects() { + this.spring.register(Config.class, AuthorizedClientController.class).autowire(); + ReactiveClientRegistrationRepository repository = this.spring.getContext() + .getBean(ReactiveClientRegistrationRepository.class); + ServerOAuth2AuthorizedClientRepository authorizedClientRepository = this.spring.getContext().getBean(ServerOAuth2AuthorizedClientRepository.class); + when(repository.findByRegistrationId(any())).thenReturn(Mono.just(TestClientRegistrations.clientRegistration().build())); + when(authorizedClientRepository.loadAuthorizedClient(any(), any(), any())).thenReturn(Mono.empty()); + + this.client.get().uri("/") + .exchange() + .expectStatus().is3xxRedirection(); + } + + @EnableWebFlux + @EnableWebFluxSecurity + static class Config { + @Bean + SecurityWebFilterChain springSecurity(ServerHttpSecurity http) { + http + .oauth2() + .client(); + return http.build(); + } + + @Bean + ReactiveClientRegistrationRepository clientRegistrationRepository() { + return mock(ReactiveClientRegistrationRepository.class); + } + + @Bean + ServerOAuth2AuthorizedClientRepository authorizedClientRepository() { + return mock(ServerOAuth2AuthorizedClientRepository.class); + } + } + + @RestController + static class AuthorizedClientController { + @GetMapping("/") + String home(@RegisteredOAuth2AuthorizedClient("github") OAuth2AuthorizedClient authorizedClient) { + return "home"; + } + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/OAuth2AuthorizationCodeGrantWebFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/OAuth2AuthorizationCodeGrantWebFilter.java new file mode 100644 index 0000000000..25ab879773 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/OAuth2AuthorizationCodeGrantWebFilter.java @@ -0,0 +1,167 @@ +/* + * Copyright 2002-2018 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 + * + * http://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.client.web.server; + +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +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.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken; +import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.web.server.WebFilterExchange; +import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler; +import org.springframework.security.web.server.authentication.ServerAuthenticationConverter; +import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler; +import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler; +import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +/** + * A {@code Filter} for the OAuth 2.0 Authorization Code Grant, + * which handles the processing of the OAuth 2.0 Authorization Response. + * + *

+ * The OAuth 2.0 Authorization Response is processed as follows: + * + *

+ * + * @author Rob Winch + * @since 5.1 + * @see OAuth2AuthorizationCodeAuthenticationToken + * @see org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeReactiveAuthenticationManager + * @see OAuth2AuthorizationRequest + * @see OAuth2AuthorizationResponse + * @see AuthorizationRequestRepository + * @see org.springframework.security.oauth2.client.web.server.OAuth2AuthorizationRequestRedirectWebFilter + * @see ReactiveClientRegistrationRepository + * @see OAuth2AuthorizedClient + * @see ServerOAuth2AuthorizedClientRepository + * @see Section 4.1 Authorization Code Grant + * @see Section 4.1.2 Authorization Response + */ +public class OAuth2AuthorizationCodeGrantWebFilter implements WebFilter { + private final ReactiveAuthenticationManager authenticationManager; + + private final ServerOAuth2AuthorizedClientRepository authorizedClientRepository; + + private ServerAuthenticationSuccessHandler authenticationSuccessHandler; + + private ServerAuthenticationConverter authenticationConverter; + + private ServerAuthenticationFailureHandler authenticationFailureHandler; + + private ServerWebExchangeMatcher requiresAuthenticationMatcher; + + private AnonymousAuthenticationToken anonymousToken = new AnonymousAuthenticationToken("key", "anonymous", + AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); + + public OAuth2AuthorizationCodeGrantWebFilter( + ReactiveAuthenticationManager authenticationManager, + ReactiveClientRegistrationRepository clientRegistrationRepository, + ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { + Assert.notNull(authenticationManager, "authenticationManager cannot be null"); + Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); + Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null"); + this.authenticationManager = authenticationManager; + this.authorizedClientRepository = authorizedClientRepository; + this.requiresAuthenticationMatcher = new PathPatternParserServerWebExchangeMatcher("/authorize/oauth2/code/{registrationId}"); + this.authenticationConverter = new ServerOAuth2AuthorizationCodeAuthenticationTokenConverter(clientRegistrationRepository); + this.authenticationSuccessHandler = new RedirectServerAuthenticationSuccessHandler(); + this.authenticationFailureHandler = (webFilterExchange, exception) -> Mono.error(exception); + } + + public OAuth2AuthorizationCodeGrantWebFilter( + ReactiveAuthenticationManager authenticationManager, + ServerAuthenticationConverter authenticationConverter, + ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { + Assert.notNull(authenticationManager, "authenticationManager cannot be null"); + Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); + Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null"); + this.authenticationManager = authenticationManager; + this.authorizedClientRepository = authorizedClientRepository; + this.requiresAuthenticationMatcher = new PathPatternParserServerWebExchangeMatcher("/authorize/oauth2/code/{registrationId}"); + this.authenticationConverter = authenticationConverter; + this.authenticationSuccessHandler = new RedirectServerAuthenticationSuccessHandler(); + this.authenticationFailureHandler = (webFilterExchange, exception) -> Mono.error(exception); + } + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + return this.requiresAuthenticationMatcher.matches(exchange) + .filter( matchResult -> matchResult.isMatch()) + .flatMap( matchResult -> this.authenticationConverter.convert(exchange)) + .switchIfEmpty(chain.filter(exchange).then(Mono.empty())) + .flatMap( token -> authenticate(exchange, chain, token)); + } + + private Mono authenticate(ServerWebExchange exchange, + WebFilterChain chain, Authentication token) { + WebFilterExchange webFilterExchange = new WebFilterExchange(exchange, chain); + return this.authenticationManager.authenticate(token) + .switchIfEmpty(Mono.defer(() -> Mono.error(new IllegalStateException("No provider found for " + token.getClass())))) + .flatMap(authentication -> onAuthenticationSuccess(authentication, webFilterExchange)) + .onErrorResume(AuthenticationException.class, e -> this.authenticationFailureHandler + .onAuthenticationFailure(webFilterExchange, e)); + } + + private Mono onAuthenticationSuccess(Authentication authentication, WebFilterExchange webFilterExchange) { + OAuth2AuthorizationCodeAuthenticationToken authenticationResult = (OAuth2AuthorizationCodeAuthenticationToken) authentication; + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + authenticationResult.getClientRegistration(), + authenticationResult.getName(), + authenticationResult.getAccessToken(), + authenticationResult.getRefreshToken()); + return this.authenticationSuccessHandler + .onAuthenticationSuccess(webFilterExchange, authentication) + .then(ReactiveSecurityContextHolder.getContext() + .map(SecurityContext::getAuthentication) + .defaultIfEmpty(this.anonymousToken) + .flatMap(principal -> this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, principal, webFilterExchange.getExchange())) + ); + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/OAuth2AuthorizationCodeGrantWebFilterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/OAuth2AuthorizationCodeGrantWebFilterTests.java new file mode 100644 index 0000000000..c0dccd99eb --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/OAuth2AuthorizationCodeGrantWebFilterTests.java @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2018 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 + * + * http://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.client.web.server; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken; +import org.springframework.security.oauth2.client.authentication.TestOAuth2AuthorizationCodeAuthenticationTokens; +import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; +import org.springframework.security.web.server.authentication.ServerAuthenticationConverter; +import org.springframework.web.server.handler.DefaultWebFilterChain; +import reactor.core.publisher.Mono; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +/** + * @author Rob Winch + * @since 5.1 + */ +@RunWith(MockitoJUnitRunner.class) +public class OAuth2AuthorizationCodeGrantWebFilterTests { + private OAuth2AuthorizationCodeGrantWebFilter filter; + @Mock + private ReactiveAuthenticationManager authenticationManager; + @Mock + private ReactiveClientRegistrationRepository clientRegistrationRepository; + @Mock + private ServerOAuth2AuthorizedClientRepository authorizedClientRepository; + + @Before + public void setup() { + this.filter = new OAuth2AuthorizationCodeGrantWebFilter( + this.authenticationManager, this.clientRegistrationRepository, + this.authorizedClientRepository); + } + + @Test + public void constructorWhenAuthenticationManagerNullThenIllegalArgumentException() { + this.authenticationManager = null; + assertThatCode(() -> new OAuth2AuthorizationCodeGrantWebFilter( + this.authenticationManager, this.clientRegistrationRepository, + this.authorizedClientRepository)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void constructorWhenClientRegistrationRepositoryNullThenIllegalArgumentException() { + this.clientRegistrationRepository = null; + assertThatCode(() -> new OAuth2AuthorizationCodeGrantWebFilter( + this.authenticationManager, this.clientRegistrationRepository, + this.authorizedClientRepository)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void constructorWhenAuthorizedClientRepositoryNullThenIllegalArgumentException() { + this.authorizedClientRepository = null; + assertThatCode(() -> new OAuth2AuthorizationCodeGrantWebFilter( + this.authenticationManager, this.clientRegistrationRepository, + this.authorizedClientRepository)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void filterWhenNotMatchThenAuthenticationManagerNotCalled() { + MockServerWebExchange exchange = MockServerWebExchange + .from(MockServerHttpRequest.get("/")); + DefaultWebFilterChain chain = new DefaultWebFilterChain( + e -> e.getResponse().setComplete()); + + this.filter.filter(exchange, chain).block(); + + verifyZeroInteractions(this.authenticationManager); + } + + @Test + public void filterWhenMatchThenAuthorizedClientSaved() { + Mono authentication = Mono + .just(TestOAuth2AuthorizationCodeAuthenticationTokens.unauthenticated()); + OAuth2AuthorizationCodeAuthenticationToken authenticated = TestOAuth2AuthorizationCodeAuthenticationTokens + .authenticated(); + ServerAuthenticationConverter converter = e -> authentication; + this.filter = new OAuth2AuthorizationCodeGrantWebFilter( + this.authenticationManager, converter, this.authorizedClientRepository); + MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest + .get("/authorize/oauth2/code/registration-id")); + DefaultWebFilterChain chain = new DefaultWebFilterChain( + e -> e.getResponse().setComplete()); + when(this.authenticationManager.authenticate(any())).thenReturn(Mono.just( + authenticated)); + when(this.authorizedClientRepository.saveAuthorizedClient(any(), any(), any())) + .thenReturn(Mono.empty()); + + this.filter.filter(exchange, chain).block(); + + verify(this.authorizedClientRepository).saveAuthorizedClient(any(), any(AnonymousAuthenticationToken.class), any()); + + } +}