2
0
Эх сурвалжийг харах

Add OAuth2AuthorizationCodeGrantWebFilter

Issue: gh-5620
Rob Winch 7 жил өмнө
parent
commit
b9ab4929b7

+ 106 - 1
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();
 	 *  }
 	 * </pre>
@@ -597,6 +599,106 @@ public class ServerHttpSecurity {
 	public class OAuth2Spec {
 		private ResourceServerSpec resourceServer;
 
+		private OAuth2ClientSpec client;
+
+		/**
+		 * Configures the OAuth2 client.
+		 *
+		 * <pre class="code">
+		 *  &#064;Bean
+		 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
+		 *      http
+		 *          // ...
+		 *          .oauth2()
+		 *              .client()
+		 *                  .clientRegistrationRepository(clientRegistrationRepository)
+		 *                  .authorizedClientRepository(authorizedClientRepository);
+		 *      return http.build();
+		 *  }
+		 * </pre>
+		 *
+		 *
+		 * @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() {}

+ 121 - 0
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";
+		}
+	}
+}

+ 167 - 0
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.
+ *
+ * <p>
+ * The OAuth 2.0 Authorization Response is processed as follows:
+ *
+ * <ul>
+ * <li>
+ *	Assuming the End-User (Resource Owner) has granted access to the Client, the Authorization Server will append the
+ *	{@link OAuth2ParameterNames#CODE code} and {@link OAuth2ParameterNames#STATE state} parameters
+ *	to the {@link OAuth2ParameterNames#REDIRECT_URI redirect_uri} (provided in the Authorization Request)
+ *	and redirect the End-User's user-agent back to this {@code Filter} (the Client).
+ * </li>
+ * <li>
+ *  This {@code Filter} will then create an {@link OAuth2AuthorizationCodeAuthenticationToken} with
+ *  the {@link OAuth2ParameterNames#CODE code} received and
+ *  delegate it to the {@link ReactiveAuthenticationManager} to authenticate.
+ * </li>
+ * <li>
+ *  Upon a successful authentication, an {@link OAuth2AuthorizedClient Authorized Client} is created by associating the
+ *  {@link OAuth2AuthorizationCodeAuthenticationToken#getClientRegistration() client} to the
+ *  {@link OAuth2AuthorizationCodeAuthenticationToken#getAccessToken() access token} and current {@code Principal}
+ *  and saving it via the {@link ServerOAuth2AuthorizedClientRepository}.
+ * </li>
+ * </ul>
+ *
+ * @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 <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant</a>
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.2">Section 4.1.2 Authorization Response</a>
+ */
+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<Void> 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<Void> 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<Void> 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()))
+					);
+	}
+}

+ 125 - 0
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> 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());
+
+	}
+}