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: + * + *