From 982fc360b2e2ed6d48188fb29184598536bb2e18 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Tue, 30 Jan 2018 15:18:03 -0500 Subject: [PATCH] Add support for authorization_code grant Fixes gh-4928 --- .../web/builders/FilterComparator.java | 4 + .../annotation/web/builders/HttpSecurity.java | 32 +- .../configurers/oauth2/OAuth2Configurer.java | 52 +++ .../client/ImplicitGrantConfigurer.java | 16 +- .../oauth2/client/OAuth2ClientConfigurer.java | 295 +++++++++++++++++ .../client/OAuth2ClientConfigurerUtils.java | 74 +++++ .../oauth2/client/OAuth2LoginConfigurer.java | 46 +-- .../oauth2/client/CommonOAuth2Provider.java | 10 +- .../client/OAuth2ClientConfigurerTests.java | 171 ++++++++++ .../client/CommonOAuth2ProviderTests.java | 10 +- .../ClientAuthorizationRequiredException.java | 60 ++++ .../oauth2/client/OAuth2ClientException.java | 44 +++ ...thorizationCodeAuthenticationProvider.java | 88 +++++ ...2AuthorizationCodeAuthenticationToken.java | 114 +++++++ .../OAuth2AuthorizationExchangeValidator.java | 54 ++++ .../OAuth2LoginAuthenticationProvider.java | 27 +- .../OAuth2AuthorizationCodeGrantFilter.java | 209 ++++++++++++ ...th2AuthorizationRequestRedirectFilter.java | 103 +++++- .../web/OAuth2AuthorizationResponseUtils.java | 72 +++++ .../web/OAuth2LoginAuthenticationFilter.java | 41 +-- ...zationCodeAuthenticationProviderTests.java | 141 ++++++++ ...orizationCodeAuthenticationTokenTests.java | 115 +++++++ ...uth2AuthorizationCodeGrantFilterTests.java | 304 ++++++++++++++++++ ...thorizationRequestRedirectFilterTests.java | 131 +++++++- .../OAuth2LoginAuthenticationFilterTests.java | 43 +-- samples/boot/oauth2/authcodegrant/README.adoc | 64 ++++ ...y-samples-boot-oauth2-authcodegrant.gradle | 16 + ...uthorizationCodeGrantApplicationTests.java | 165 ++++++++++ ...uth2AuthorizationCodeGrantApplication.java | 30 ++ .../java/sample/config/SecurityConfig.java | 55 ++++ .../main/java/sample/web/MainController.java | 81 +++++ .../src/main/resources/application.yml | 23 ++ .../resources/templates/github-repos.html | 28 ++ 33 files changed, 2531 insertions(+), 187 deletions(-) create mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java create mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java create mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java create mode 100644 config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ClientAuthorizationRequiredException.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2ClientException.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationToken.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationExchangeValidator.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationResponseUtils.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationTokenTests.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilterTests.java create mode 100644 samples/boot/oauth2/authcodegrant/README.adoc create mode 100644 samples/boot/oauth2/authcodegrant/spring-security-samples-boot-oauth2-authcodegrant.gradle create mode 100644 samples/boot/oauth2/authcodegrant/src/integration-test/java/org/springframework/security/samples/OAuth2AuthorizationCodeGrantApplicationTests.java create mode 100644 samples/boot/oauth2/authcodegrant/src/main/java/sample/OAuth2AuthorizationCodeGrantApplication.java create mode 100644 samples/boot/oauth2/authcodegrant/src/main/java/sample/config/SecurityConfig.java create mode 100644 samples/boot/oauth2/authcodegrant/src/main/java/sample/web/MainController.java create mode 100644 samples/boot/oauth2/authcodegrant/src/main/resources/application.yml create mode 100644 samples/boot/oauth2/authcodegrant/src/main/resources/templates/github-repos.html diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java index 632da9b14d..20f336d609 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java @@ -117,6 +117,10 @@ final class FilterComparator implements Comparator, Serializable { order += STEP; put(AnonymousAuthenticationFilter.class, order); order += STEP; + filterToOrder.put( + "org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter", + order); + order += STEP; put(SessionManagementFilter.class, order); order += STEP; put(ExceptionTranslationFilter.class, order); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java index 319b6967a0..da2416d4b7 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * 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. @@ -15,14 +15,6 @@ */ package org.springframework.security.config.annotation.web.builders; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import javax.servlet.Filter; -import javax.servlet.http.HttpServletRequest; - import org.springframework.context.ApplicationContext; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; @@ -56,12 +48,13 @@ import org.springframework.security.config.annotation.web.configurers.SecurityCo import org.springframework.security.config.annotation.web.configurers.ServletApiConfigurer; import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer; import org.springframework.security.config.annotation.web.configurers.X509Configurer; +import org.springframework.security.config.annotation.web.configurers.oauth2.OAuth2Configurer; +import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer; import org.springframework.security.config.annotation.web.configurers.openid.OpenIDLoginConfigurer; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer; import org.springframework.security.web.DefaultSecurityFilterChain; import org.springframework.security.web.PortMapper; import org.springframework.security.web.PortMapperImpl; @@ -79,6 +72,13 @@ import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.filter.CorsFilter; import org.springframework.web.servlet.handler.HandlerMappingIntrospector; +import javax.servlet.Filter; +import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + /** * A {@link HttpSecurity} is similar to Spring Security's XML <http> element in the * namespace configuration. It allows configuring web based security for specific http @@ -991,6 +991,18 @@ public final class HttpSecurity extends return getOrApply(new OAuth2LoginConfigurer<>()); } + /** + * Configures support for the OAuth 2.0 Authorization Framework. + * + * @author Joe Grandja + * @since 5.1 + * @return the {@link OAuth2Configurer} for further customizations + * @throws Exception + */ + public OAuth2Configurer oauth2() throws Exception { + return getOrApply(new OAuth2Configurer()); + } + /** * Configures channel security. In order for this configuration to be useful at least * one mapping to a required channel must be provided. diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java new file mode 100644 index 0000000000..60b5e49855 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java @@ -0,0 +1,52 @@ +/* + * 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.annotation.web.configurers.oauth2; + +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2ClientConfigurer; + +/** + * An {@link AbstractHttpConfigurer} that provides support for the + * OAuth 2.0 Authorization Framework. + * + * @author Joe Grandja + * @since 5.1 + * @see HttpSecurity#oauth2() + * @see OAuth2ClientConfigurer + * @see AbstractHttpConfigurer + */ +public final class OAuth2Configurer extends AbstractHttpConfigurer { + + /** + * Returns the {@link OAuth2ClientConfigurer} for configuring OAuth 2.0 Client support. + * + * @return the {@link OAuth2ClientConfigurer} + * @throws Exception + */ + public OAuth2ClientConfigurer client() throws Exception { + return this.getOrApply(new OAuth2ClientConfigurer<>()); + } + + @SuppressWarnings("unchecked") + private > C getOrApply(C configurer) throws Exception { + C existingConfigurer = (C) this.getBuilder().getConfigurer(configurer.getClass()); + if (existingConfigurer != null) { + return existingConfigurer; + } + return this.getBuilder().apply(configurer); + } +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/ImplicitGrantConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/ImplicitGrantConfigurer.java index b860818a67..3917f522ec 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/ImplicitGrantConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/ImplicitGrantConfigurer.java @@ -15,7 +15,6 @@ */ package org.springframework.security.config.annotation.web.configurers.oauth2.client; -import org.springframework.context.ApplicationContext; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; @@ -86,7 +85,7 @@ public final class ImplicitGrantConfigurer> ext @Override public void configure(B http) throws Exception { OAuth2AuthorizationRequestRedirectFilter authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter( - this.getClientRegistrationRepository(), this.getAuthorizationRequestBaseUri()); + OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder()), this.getAuthorizationRequestBaseUri()); http.addFilter(this.postProcess(authorizationRequestFilter)); } @@ -95,17 +94,4 @@ public final class ImplicitGrantConfigurer> ext this.authorizationRequestBaseUri : OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI; } - - private ClientRegistrationRepository getClientRegistrationRepository() { - ClientRegistrationRepository clientRegistrationRepository = this.getBuilder().getSharedObject(ClientRegistrationRepository.class); - if (clientRegistrationRepository == null) { - clientRegistrationRepository = this.getClientRegistrationRepositoryBean(); - this.getBuilder().setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository); - } - return clientRegistrationRepository; - } - - private ClientRegistrationRepository getClientRegistrationRepositoryBean() { - return this.getBuilder().getSharedObject(ApplicationContext.class).getBean(ClientRegistrationRepository.class); - } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java new file mode 100644 index 0000000000..79ab1adc27 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java @@ -0,0 +1,295 @@ +/* + * 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.annotation.web.configurers.oauth2.client; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationProvider; +import org.springframework.security.oauth2.client.endpoint.NimbusAuthorizationCodeTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.util.Assert; + +/** + * An {@link AbstractHttpConfigurer} for OAuth 2.0 Client support. + * + *

+ * The following configuration options are available: + * + *

    + *
  • {@link #authorizationCodeGrant()} - enables the OAuth 2.0 Authorization Code Grant
  • + *
+ * + *

+ * Defaults are provided for all configuration options with the only required configuration + * being {@link #clientRegistrationRepository(ClientRegistrationRepository)}. + * Alternatively, a {@link ClientRegistrationRepository} {@code @Bean} may be registered instead. + * + *

Security Filters

+ * + * The following {@code Filter}'s are populated when {@link #authorizationCodeGrant()} is configured: + * + *
    + *
  • {@link OAuth2AuthorizationRequestRedirectFilter}
  • + *
  • {@link OAuth2AuthorizationCodeGrantFilter}
  • + *
+ * + *

Shared Objects Created

+ * + * The following shared objects are populated: + * + *
    + *
  • {@link ClientRegistrationRepository} (required)
  • + *
  • {@link OAuth2AuthorizedClientService} (optional)
  • + *
+ * + *

Shared Objects Used

+ * + * The following shared objects are used: + * + *
    + *
  • {@link ClientRegistrationRepository}
  • + *
  • {@link OAuth2AuthorizedClientService}
  • + *
+ * + * @author Joe Grandja + * @since 5.1 + * @see OAuth2AuthorizationRequestRedirectFilter + * @see OAuth2AuthorizationCodeGrantFilter + * @see ClientRegistrationRepository + * @see OAuth2AuthorizedClientService + * @see AbstractHttpConfigurer + */ +public final class OAuth2ClientConfigurer> extends + AbstractHttpConfigurer, B> { + + private AuthorizationCodeGrantConfigurer authorizationCodeGrantConfigurer; + + /** + * Sets the repository of client registrations. + * + * @param clientRegistrationRepository the repository of client registrations + * @return the {@link OAuth2ClientConfigurer} for further configuration + */ + public OAuth2ClientConfigurer clientRegistrationRepository(ClientRegistrationRepository clientRegistrationRepository) { + Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); + this.getBuilder().setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository); + return this; + } + + /** + * Sets the service for authorized client(s). + * + * @param authorizedClientService the authorized client service + * @return the {@link OAuth2ClientConfigurer} for further configuration + */ + public OAuth2ClientConfigurer authorizedClientService(OAuth2AuthorizedClientService authorizedClientService) { + Assert.notNull(authorizedClientService, "authorizedClientService cannot be null"); + this.getBuilder().setSharedObject(OAuth2AuthorizedClientService.class, authorizedClientService); + return this; + } + + /** + * Returns the {@link AuthorizationCodeGrantConfigurer} for configuring the OAuth 2.0 Authorization Code Grant. + * + * @return the {@link AuthorizationCodeGrantConfigurer} + */ + public AuthorizationCodeGrantConfigurer authorizationCodeGrant() { + if (this.authorizationCodeGrantConfigurer == null) { + this.authorizationCodeGrantConfigurer = new AuthorizationCodeGrantConfigurer(); + } + return this.authorizationCodeGrantConfigurer; + } + + /** + * Configuration options for the OAuth 2.0 Authorization Code Grant. + */ + public class AuthorizationCodeGrantConfigurer { + private final AuthorizationEndpointConfig authorizationEndpointConfig = new AuthorizationEndpointConfig(); + private final TokenEndpointConfig tokenEndpointConfig = new TokenEndpointConfig(); + + private AuthorizationCodeGrantConfigurer() { + } + + /** + * Returns the {@link AuthorizationEndpointConfig} for configuring the Authorization Server's Authorization Endpoint. + * + * @return the {@link AuthorizationEndpointConfig} + */ + public AuthorizationEndpointConfig authorizationEndpoint() { + return this.authorizationEndpointConfig; + } + + /** + * Configuration options for the Authorization Server's Authorization Endpoint. + */ + public class AuthorizationEndpointConfig { + private String authorizationRequestBaseUri; + private AuthorizationRequestRepository authorizationRequestRepository; + + private AuthorizationEndpointConfig() { + } + + /** + * Sets the base {@code URI} used for authorization requests. + * + * @param authorizationRequestBaseUri the base {@code URI} used for authorization requests + * @return the {@link AuthorizationEndpointConfig} for further configuration + */ + public AuthorizationEndpointConfig baseUri(String authorizationRequestBaseUri) { + Assert.hasText(authorizationRequestBaseUri, "authorizationRequestBaseUri cannot be empty"); + this.authorizationRequestBaseUri = authorizationRequestBaseUri; + return this; + } + + /** + * Sets the repository used for storing {@link OAuth2AuthorizationRequest}'s. + * + * @param authorizationRequestRepository the repository used for storing {@link OAuth2AuthorizationRequest}'s + * @return the {@link AuthorizationEndpointConfig} for further configuration + */ + public AuthorizationEndpointConfig authorizationRequestRepository( + AuthorizationRequestRepository authorizationRequestRepository) { + + Assert.notNull(authorizationRequestRepository, "authorizationRequestRepository cannot be null"); + this.authorizationRequestRepository = authorizationRequestRepository; + return this; + } + + /** + * Returns the {@link AuthorizationCodeGrantConfigurer} for further configuration. + * + * @return the {@link AuthorizationCodeGrantConfigurer} + */ + public AuthorizationCodeGrantConfigurer and() { + return AuthorizationCodeGrantConfigurer.this; + } + } + + /** + * Returns the {@link TokenEndpointConfig} for configuring the Authorization Server's Token Endpoint. + * + * @return the {@link TokenEndpointConfig} + */ + public TokenEndpointConfig tokenEndpoint() { + return this.tokenEndpointConfig; + } + + /** + * Configuration options for the Authorization Server's Token Endpoint. + */ + public class TokenEndpointConfig { + private OAuth2AccessTokenResponseClient accessTokenResponseClient; + + private TokenEndpointConfig() { + } + + /** + * Sets the client used for requesting the access token credential from the Token Endpoint. + * + * @param accessTokenResponseClient the client used for requesting the access token credential from the Token Endpoint + * @return the {@link TokenEndpointConfig} for further configuration + */ + public TokenEndpointConfig accessTokenResponseClient( + OAuth2AccessTokenResponseClient accessTokenResponseClient) { + + Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null"); + this.accessTokenResponseClient = accessTokenResponseClient; + return this; + } + + /** + * Returns the {@link AuthorizationCodeGrantConfigurer} for further configuration. + * + * @return the {@link AuthorizationCodeGrantConfigurer} + */ + public AuthorizationCodeGrantConfigurer and() { + return AuthorizationCodeGrantConfigurer.this; + } + } + + /** + * Returns the {@link OAuth2ClientConfigurer} for further configuration. + * + * @return the {@link OAuth2ClientConfigurer} + */ + public OAuth2ClientConfigurer and() { + return OAuth2ClientConfigurer.this; + } + } + + @Override + public void init(B builder) throws Exception { + if (this.authorizationCodeGrantConfigurer != null) { + this.init(builder, this.authorizationCodeGrantConfigurer); + } + } + + @Override + public void configure(B builder) throws Exception { + if (this.authorizationCodeGrantConfigurer != null) { + this.configure(builder, this.authorizationCodeGrantConfigurer); + } + } + + private void init(B builder, AuthorizationCodeGrantConfigurer authorizationCodeGrantConfigurer) throws Exception { + OAuth2AccessTokenResponseClient accessTokenResponseClient = + authorizationCodeGrantConfigurer.tokenEndpointConfig.accessTokenResponseClient; + if (accessTokenResponseClient == null) { + accessTokenResponseClient = new NimbusAuthorizationCodeTokenResponseClient(); + } + + OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider = + new OAuth2AuthorizationCodeAuthenticationProvider(accessTokenResponseClient); + builder.authenticationProvider(this.postProcess(authorizationCodeAuthenticationProvider)); + } + + private void configure(B builder, AuthorizationCodeGrantConfigurer authorizationCodeGrantConfigurer) throws Exception { + String authorizationRequestBaseUri = authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestBaseUri; + if (authorizationRequestBaseUri == null) { + authorizationRequestBaseUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI; + } + + OAuth2AuthorizationRequestRedirectFilter authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter( + OAuth2ClientConfigurerUtils.getClientRegistrationRepository(builder), authorizationRequestBaseUri); + + if (authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestRepository != null) { + authorizationRequestFilter.setAuthorizationRequestRepository( + authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestRepository); + } + builder.addFilter(this.postProcess(authorizationRequestFilter)); + + AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class); + + OAuth2AuthorizationCodeGrantFilter authorizationCodeGrantFilter = new OAuth2AuthorizationCodeGrantFilter( + OAuth2ClientConfigurerUtils.getClientRegistrationRepository(builder), + OAuth2ClientConfigurerUtils.getAuthorizedClientService(builder), + authenticationManager); + + if (authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestRepository != null) { + authorizationCodeGrantFilter.setAuthorizationRequestRepository( + authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestRepository); + } + builder.addFilter(this.postProcess(authorizationCodeGrantFilter)); + } +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java new file mode 100644 index 0000000000..646c32accf --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java @@ -0,0 +1,74 @@ +/* + * 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.annotation.web.configurers.oauth2.client; + +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; + +import java.util.Map; + +/** + * Utility methods for the OAuth 2.0 Client {@link AbstractHttpConfigurer}'s. + * + * @author Joe Grandja + * @since 5.1 + */ +final class OAuth2ClientConfigurerUtils { + + private OAuth2ClientConfigurerUtils() { + } + + static > ClientRegistrationRepository getClientRegistrationRepository(B builder) { + ClientRegistrationRepository clientRegistrationRepository = builder.getSharedObject(ClientRegistrationRepository.class); + if (clientRegistrationRepository == null) { + clientRegistrationRepository = getClientRegistrationRepositoryBean(builder); + builder.setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository); + } + return clientRegistrationRepository; + } + + private static > ClientRegistrationRepository getClientRegistrationRepositoryBean(B builder) { + return builder.getSharedObject(ApplicationContext.class).getBean(ClientRegistrationRepository.class); + } + + static > OAuth2AuthorizedClientService getAuthorizedClientService(B builder) { + OAuth2AuthorizedClientService authorizedClientService = builder.getSharedObject(OAuth2AuthorizedClientService.class); + if (authorizedClientService == null) { + authorizedClientService = getAuthorizedClientServiceBean(builder); + if (authorizedClientService == null) { + authorizedClientService = new InMemoryOAuth2AuthorizedClientService(getClientRegistrationRepository(builder)); + } + builder.setSharedObject(OAuth2AuthorizedClientService.class, authorizedClientService); + } + return authorizedClientService; + } + + private static > OAuth2AuthorizedClientService getAuthorizedClientServiceBean(B builder) { + Map authorizedClientServiceMap = BeanFactoryUtils.beansOfTypeIncludingAncestors( + builder.getSharedObject(ApplicationContext.class), OAuth2AuthorizedClientService.class); + if (authorizedClientServiceMap.size() > 1) { + throw new NoUniqueBeanDefinitionException(OAuth2AuthorizedClientService.class, authorizedClientServiceMap.size(), + "Only one matching @Bean of type " + OAuth2AuthorizedClientService.class.getName() + " should be registered."); + } + return (!authorizedClientServiceMap.isEmpty() ? authorizedClientServiceMap.values().iterator().next() : null); + } +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java index 64ae269667..7b924691f8 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java @@ -26,7 +26,6 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; -import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationProvider; import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken; @@ -376,8 +375,8 @@ public final class OAuth2LoginConfigurer> exten public void init(B http) throws Exception { OAuth2LoginAuthenticationFilter authenticationFilter = new OAuth2LoginAuthenticationFilter( - this.getClientRegistrationRepository(), - this.getAuthorizedClientService(), + OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder()), + OAuth2ClientConfigurerUtils.getAuthorizedClientService(this.getBuilder()), OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI); this.setAuthenticationFilter(authenticationFilter); this.loginProcessingUrl(OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI); @@ -442,7 +441,7 @@ public final class OAuth2LoginConfigurer> exten } OAuth2AuthorizationRequestRedirectFilter authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter( - this.getClientRegistrationRepository(), authorizationRequestBaseUri); + OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder()), authorizationRequestBaseUri); if (this.authorizationEndpointConfig.authorizationRequestRepository != null) { authorizationRequestFilter.setAuthorizationRequestRepository( @@ -466,41 +465,6 @@ public final class OAuth2LoginConfigurer> exten return new AntPathRequestMatcher(loginProcessingUrl); } - private ClientRegistrationRepository getClientRegistrationRepository() { - ClientRegistrationRepository clientRegistrationRepository = - this.getBuilder().getSharedObject(ClientRegistrationRepository.class); - if (clientRegistrationRepository == null) { - clientRegistrationRepository = this.getClientRegistrationRepositoryBean(); - this.getBuilder().setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository); - } - return clientRegistrationRepository; - } - - private ClientRegistrationRepository getClientRegistrationRepositoryBean() { - return this.getBuilder().getSharedObject(ApplicationContext.class).getBean(ClientRegistrationRepository.class); - } - - private OAuth2AuthorizedClientService getAuthorizedClientService() { - OAuth2AuthorizedClientService authorizedClientService = - this.getBuilder().getSharedObject(OAuth2AuthorizedClientService.class); - if (authorizedClientService == null) { - authorizedClientService = this.getAuthorizedClientServiceBean(); - if (authorizedClientService == null) { - authorizedClientService = new InMemoryOAuth2AuthorizedClientService(this.getClientRegistrationRepository()); - } - this.getBuilder().setSharedObject(OAuth2AuthorizedClientService.class, authorizedClientService); - } - return authorizedClientService; - } - - private OAuth2AuthorizedClientService getAuthorizedClientServiceBean() { - Map authorizedClientServiceMap = - BeanFactoryUtils.beansOfTypeIncludingAncestors( - this.getBuilder().getSharedObject(ApplicationContext.class), - OAuth2AuthorizedClientService.class); - return (!authorizedClientServiceMap.isEmpty() ? authorizedClientServiceMap.values().iterator().next() : null); - } - private GrantedAuthoritiesMapper getGrantedAuthoritiesMapper() { GrantedAuthoritiesMapper grantedAuthoritiesMapper = this.getBuilder().getSharedObject(GrantedAuthoritiesMapper.class); @@ -528,7 +492,8 @@ public final class OAuth2LoginConfigurer> exten } Iterable clientRegistrations = null; - ClientRegistrationRepository clientRegistrationRepository = this.getClientRegistrationRepository(); + ClientRegistrationRepository clientRegistrationRepository = + OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder()); ResolvableType type = ResolvableType.forInstance(clientRegistrationRepository).as(Iterable.class); if (type != ResolvableType.NONE && ClientRegistration.class.isAssignableFrom(type.resolveGenerics()[0])) { clientRegistrations = (Iterable) clientRegistrationRepository; @@ -580,5 +545,4 @@ public final class OAuth2LoginConfigurer> exten return OAuth2LoginAuthenticationToken.class.isAssignableFrom(authentication); } } - } diff --git a/config/src/main/java/org/springframework/security/config/oauth2/client/CommonOAuth2Provider.java b/config/src/main/java/org/springframework/security/config/oauth2/client/CommonOAuth2Provider.java index ac33f1f636..526a05cc5f 100644 --- a/config/src/main/java/org/springframework/security/config/oauth2/client/CommonOAuth2Provider.java +++ b/config/src/main/java/org/springframework/security/config/oauth2/client/CommonOAuth2Provider.java @@ -36,7 +36,7 @@ public enum CommonOAuth2Provider { @Override public Builder getBuilder(String registrationId) { ClientRegistration.Builder builder = getBuilder(registrationId, - ClientAuthenticationMethod.BASIC, DEFAULT_LOGIN_REDIRECT_URL); + ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL); builder.scope("openid", "profile", "email"); builder.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth"); builder.tokenUri("https://www.googleapis.com/oauth2/v4/token"); @@ -53,7 +53,7 @@ public enum CommonOAuth2Provider { @Override public Builder getBuilder(String registrationId) { ClientRegistration.Builder builder = getBuilder(registrationId, - ClientAuthenticationMethod.BASIC, DEFAULT_LOGIN_REDIRECT_URL); + ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL); builder.scope("read:user"); builder.authorizationUri("https://github.com/login/oauth/authorize"); builder.tokenUri("https://github.com/login/oauth/access_token"); @@ -69,7 +69,7 @@ public enum CommonOAuth2Provider { @Override public Builder getBuilder(String registrationId) { ClientRegistration.Builder builder = getBuilder(registrationId, - ClientAuthenticationMethod.POST, DEFAULT_LOGIN_REDIRECT_URL); + ClientAuthenticationMethod.POST, DEFAULT_REDIRECT_URL); builder.scope("public_profile", "email"); builder.authorizationUri("https://www.facebook.com/v2.8/dialog/oauth"); builder.tokenUri("https://graph.facebook.com/v2.8/oauth/access_token"); @@ -85,7 +85,7 @@ public enum CommonOAuth2Provider { @Override public Builder getBuilder(String registrationId) { ClientRegistration.Builder builder = getBuilder(registrationId, - ClientAuthenticationMethod.BASIC, DEFAULT_LOGIN_REDIRECT_URL); + ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL); builder.scope("openid", "profile", "email", "address", "phone"); builder.userNameAttributeName(IdTokenClaimNames.SUB); builder.clientName("Okta"); @@ -93,7 +93,7 @@ public enum CommonOAuth2Provider { } }; - private static final String DEFAULT_LOGIN_REDIRECT_URL = "{baseUrl}/login/oauth2/code/{registrationId}"; + private static final String DEFAULT_REDIRECT_URL = "{baseUrl}/{action}/oauth2/code/{registrationId}"; protected final ClientRegistration.Builder getBuilder(String registrationId, ClientAuthenticationMethod method, String redirectUri) { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java new file mode 100644 index 0000000000..fd015321a7 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java @@ -0,0 +1,171 @@ +/* + * 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.annotation.web.configurers.oauth2.client; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; +import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests for {@link OAuth2ClientConfigurer}. + * + * @author Joe Grandja + */ +public class OAuth2ClientConfigurerTests { + private static ClientRegistrationRepository clientRegistrationRepository; + + private static OAuth2AuthorizedClientService authorizedClientService; + + private static OAuth2AccessTokenResponseClient accessTokenResponseClient; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired + private MockMvc mockMvc; + + private ClientRegistration registration1; + + @Before + public void setup() { + this.registration1 = ClientRegistration.withRegistrationId("registration-1") + .clientId("client-1") + .clientSecret("secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate("{baseUrl}/client-1") + .scope("user") + .authorizationUri("https://provider.com/oauth2/authorize") + .tokenUri("https://provider.com/oauth2/token") + .userInfoUri("https://provider.com/oauth2/user") + .userNameAttributeName("id") + .clientName("client-1") + .build(); + clientRegistrationRepository = new InMemoryClientRegistrationRepository(this.registration1); + authorizedClientService = new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository); + + OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("access-token-1234") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .expiresIn(300) + .build(); + accessTokenResponseClient = mock(OAuth2AccessTokenResponseClient.class); + when(accessTokenResponseClient.getTokenResponse(any(OAuth2AuthorizationCodeGrantRequest.class))).thenReturn(accessTokenResponse); + } + + @Test + public void configureWhenAuthorizationCodeRequestThenRedirectForAuthorization() throws Exception { + this.spring.register(OAuth2ClientConfig.class).autowire(); + + MvcResult mvcResult = this.mockMvc.perform(get("/oauth2/authorization/registration-1")) + .andExpect(status().is3xxRedirection()) + .andReturn(); + assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http://localhost/client-1"); + } + + @Test + public void configureWhenAuthorizationCodeResponseSuccessThenAuthorizedClientSaved() throws Exception { + this.spring.register(OAuth2ClientConfig.class).autowire(); + + // Setup the Authorization Request in the session + Map additionalParameters = new HashMap<>(); + additionalParameters.put(OAuth2ParameterNames.REGISTRATION_ID, this.registration1.getRegistrationId()); + OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri(this.registration1.getProviderDetails().getAuthorizationUri()) + .clientId(this.registration1.getClientId()) + .redirectUri("http://localhost/client-1") + .state("state") + .additionalParameters(additionalParameters) + .build(); + + AuthorizationRequestRepository authorizationRequestRepository = + new HttpSessionOAuth2AuthorizationRequestRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response); + + MockHttpSession session = (MockHttpSession) request.getSession(); + + String principalName = "user1"; + + this.mockMvc.perform(get("/client-1") + .param(OAuth2ParameterNames.CODE, "code") + .param(OAuth2ParameterNames.STATE, "state") + .with(user(principalName)) + .session(session)) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("http://localhost/client-1")); + + OAuth2AuthorizedClient authorizedClient = authorizedClientService.loadAuthorizedClient( + this.registration1.getRegistrationId(), principalName); + assertThat(authorizedClient).isNotNull(); + } + + @EnableWebSecurity + static class OAuth2ClientConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2() + .client() + .clientRegistrationRepository(clientRegistrationRepository) + .authorizedClientService(authorizedClientService) + .authorizationCodeGrant() + .tokenEndpoint() + .accessTokenResponseClient(accessTokenResponseClient); + } + } +} diff --git a/config/src/test/java/org/springframework/security/config/oauth2/client/CommonOAuth2ProviderTests.java b/config/src/test/java/org/springframework/security/config/oauth2/client/CommonOAuth2ProviderTests.java index 148c5164ba..2e093fe94c 100644 --- a/config/src/test/java/org/springframework/security/config/oauth2/client/CommonOAuth2ProviderTests.java +++ b/config/src/test/java/org/springframework/security/config/oauth2/client/CommonOAuth2ProviderTests.java @@ -31,7 +31,7 @@ import static org.assertj.core.api.Assertions.assertThat; */ public class CommonOAuth2ProviderTests { - private static final String DEFAULT_LOGIN_REDIRECT_URL = "{baseUrl}/login/oauth2/code/{registrationId}"; + private static final String DEFAULT_REDIRECT_URL = "{baseUrl}/{action}/oauth2/code/{registrationId}"; @Test public void getBuilderWhenGoogleShouldHaveGoogleSettings() throws Exception { @@ -51,7 +51,7 @@ public class CommonOAuth2ProviderTests { .isEqualTo(ClientAuthenticationMethod.BASIC); assertThat(registration.getAuthorizationGrantType()) .isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); - assertThat(registration.getRedirectUriTemplate()).isEqualTo(DEFAULT_LOGIN_REDIRECT_URL); + assertThat(registration.getRedirectUriTemplate()).isEqualTo(DEFAULT_REDIRECT_URL); assertThat(registration.getScopes()).containsOnly("openid", "profile", "email"); assertThat(registration.getClientName()).isEqualTo("Google"); assertThat(registration.getRegistrationId()).isEqualTo("123"); @@ -74,7 +74,7 @@ public class CommonOAuth2ProviderTests { .isEqualTo(ClientAuthenticationMethod.BASIC); assertThat(registration.getAuthorizationGrantType()) .isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); - assertThat(registration.getRedirectUriTemplate()).isEqualTo(DEFAULT_LOGIN_REDIRECT_URL); + assertThat(registration.getRedirectUriTemplate()).isEqualTo(DEFAULT_REDIRECT_URL); assertThat(registration.getScopes()).containsOnly("read:user"); assertThat(registration.getClientName()).isEqualTo("GitHub"); assertThat(registration.getRegistrationId()).isEqualTo("123"); @@ -97,7 +97,7 @@ public class CommonOAuth2ProviderTests { .isEqualTo(ClientAuthenticationMethod.POST); assertThat(registration.getAuthorizationGrantType()) .isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); - assertThat(registration.getRedirectUriTemplate()).isEqualTo(DEFAULT_LOGIN_REDIRECT_URL); + assertThat(registration.getRedirectUriTemplate()).isEqualTo(DEFAULT_REDIRECT_URL); assertThat(registration.getScopes()).containsOnly("public_profile", "email"); assertThat(registration.getClientName()).isEqualTo("Facebook"); assertThat(registration.getRegistrationId()).isEqualTo("123"); @@ -122,7 +122,7 @@ public class CommonOAuth2ProviderTests { .isEqualTo(ClientAuthenticationMethod.BASIC); assertThat(registration.getAuthorizationGrantType()) .isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); - assertThat(registration.getRedirectUriTemplate()).isEqualTo(DEFAULT_LOGIN_REDIRECT_URL); + assertThat(registration.getRedirectUriTemplate()).isEqualTo(DEFAULT_REDIRECT_URL); assertThat(registration.getScopes()).containsOnly("openid", "profile", "email", "address", "phone"); assertThat(registration.getClientName()).isEqualTo("Okta"); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ClientAuthorizationRequiredException.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ClientAuthorizationRequiredException.java new file mode 100644 index 0000000000..ca2f8258e7 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ClientAuthorizationRequiredException.java @@ -0,0 +1,60 @@ +/* + * 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; + +import org.springframework.util.Assert; + +/** + * This exception is thrown when an OAuth 2.0 Client is required + * to obtain authorization from the Resource Owner. + * + * @author Joe Grandja + * @since 5.1 + * @see OAuth2AuthorizedClient + */ +public class ClientAuthorizationRequiredException extends OAuth2ClientException { + private final String clientRegistrationId; + + /** + * Constructs a {@code ClientAuthorizationRequiredException} using the provided parameters. + * + * @param clientRegistrationId the identifier for the client's registration + */ + public ClientAuthorizationRequiredException(String clientRegistrationId) { + this(clientRegistrationId, "Authorization required for Client Registration Id: " + clientRegistrationId); + } + + /** + * Constructs a {@code ClientAuthorizationRequiredException} using the provided parameters. + * + * @param clientRegistrationId the identifier for the client's registration + * @param message the detail message + */ + public ClientAuthorizationRequiredException(String clientRegistrationId, String message) { + super(message); + Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty"); + this.clientRegistrationId = clientRegistrationId; + } + + /** + * Returns the identifier for the client's registration. + * + * @return the identifier for the client's registration + */ + public String getClientRegistrationId() { + return this.clientRegistrationId; + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2ClientException.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2ClientException.java new file mode 100644 index 0000000000..fef5e49e1c --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2ClientException.java @@ -0,0 +1,44 @@ +/* + * 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; + +/** + * Base exception for OAuth 2.0 Client related errors. + * + * @author Joe Grandja + * @since 5.1 + */ +public class OAuth2ClientException extends RuntimeException { + + /** + * Constructs an {@code OAuth2ClientException} using the provided parameters. + * + * @param message the detail message + */ + public OAuth2ClientException(String message) { + super(message); + } + + /** + * Constructs an {@code OAuth2ClientException} using the provided parameters. + * + * @param message the detail message + * @param cause the root cause + */ + public OAuth2ClientException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java new file mode 100644 index 0000000000..c202a31f3e --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java @@ -0,0 +1,88 @@ +/* + * 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.authentication; + +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.util.Assert; + +/** + * An implementation of an {@link AuthenticationProvider} for the OAuth 2.0 Authorization Code Grant. + * + *

+ * This {@link AuthenticationProvider} is responsible for authenticating + * an Authorization Code credential with the Authorization Server's Token Endpoint + * and if valid, exchanging it for an Access Token credential. + * + * @author Joe Grandja + * @since 5.1 + * @see OAuth2AuthorizationCodeAuthenticationToken + * @see OAuth2AccessTokenResponseClient + * @see Section 4.1 Authorization Code Grant Flow + * @see Section 4.1.3 Access Token Request + * @see Section 4.1.4 Access Token Response + */ +public class OAuth2AuthorizationCodeAuthenticationProvider implements AuthenticationProvider { + private final OAuth2AccessTokenResponseClient accessTokenResponseClient; + + /** + * Constructs an {@code OAuth2AuthorizationCodeAuthenticationProvider} using the provided parameters. + * + * @param accessTokenResponseClient the client used for requesting the access token credential from the Token Endpoint + */ + public OAuth2AuthorizationCodeAuthenticationProvider( + OAuth2AccessTokenResponseClient accessTokenResponseClient) { + + Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null"); + this.accessTokenResponseClient = accessTokenResponseClient; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication = + (OAuth2AuthorizationCodeAuthenticationToken) authentication; + + OAuth2AuthorizationExchangeValidator.validate( + authorizationCodeAuthentication.getAuthorizationExchange()); + + OAuth2AccessTokenResponse accessTokenResponse = + this.accessTokenResponseClient.getTokenResponse( + new OAuth2AuthorizationCodeGrantRequest( + authorizationCodeAuthentication.getClientRegistration(), + authorizationCodeAuthentication.getAuthorizationExchange())); + + OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken(); + + OAuth2AuthorizationCodeAuthenticationToken authenticationResult = + new OAuth2AuthorizationCodeAuthenticationToken( + authorizationCodeAuthentication.getClientRegistration(), + authorizationCodeAuthentication.getAuthorizationExchange(), + accessToken); + authenticationResult.setDetails(authorizationCodeAuthentication.getDetails()); + + return authenticationResult; + } + + @Override + public boolean supports(Class authentication) { + return OAuth2AuthorizationCodeAuthenticationToken.class.isAssignableFrom(authentication); + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationToken.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationToken.java new file mode 100644 index 0000000000..bbc6d4aca1 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationToken.java @@ -0,0 +1,114 @@ +/* + * 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.authentication; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.SpringSecurityCoreVersion; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; +import org.springframework.util.Assert; + +import java.util.Collections; + +/** + * An {@link AbstractAuthenticationToken} for the OAuth 2.0 Authorization Code Grant. + * + * @author Joe Grandja + * @since 5.1 + * @see AbstractAuthenticationToken + * @see ClientRegistration + * @see OAuth2AuthorizationExchange + * @see OAuth2AccessToken + * @see Section 4.1 Authorization Code Grant Flow + */ +public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenticationToken { + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + private ClientRegistration clientRegistration; + private OAuth2AuthorizationExchange authorizationExchange; + private OAuth2AccessToken accessToken; + + /** + * This constructor should be used when the Authorization Request/Response is complete. + * + * @param clientRegistration the client registration + * @param authorizationExchange the authorization exchange + */ + public OAuth2AuthorizationCodeAuthenticationToken(ClientRegistration clientRegistration, + OAuth2AuthorizationExchange authorizationExchange) { + super(Collections.emptyList()); + Assert.notNull(clientRegistration, "clientRegistration cannot be null"); + Assert.notNull(authorizationExchange, "authorizationExchange cannot be null"); + this.clientRegistration = clientRegistration; + this.authorizationExchange = authorizationExchange; + } + + /** + * This constructor should be used when the Access Token Request/Response is complete, + * which indicates that the Authorization Code Grant flow has fully completed. + * + * @param clientRegistration the client registration + * @param authorizationExchange the authorization exchange + * @param accessToken the access token credential + */ + public OAuth2AuthorizationCodeAuthenticationToken(ClientRegistration clientRegistration, + OAuth2AuthorizationExchange authorizationExchange, + OAuth2AccessToken accessToken) { + this(clientRegistration, authorizationExchange); + Assert.notNull(accessToken, "accessToken cannot be null"); + this.accessToken = accessToken; + this.setAuthenticated(true); + } + + @Override + public Object getPrincipal() { + return this.clientRegistration.getClientId(); + } + + @Override + public Object getCredentials() { + return this.accessToken != null ? + this.accessToken.getTokenValue() : + this.authorizationExchange.getAuthorizationResponse().getCode(); + } + + /** + * Returns the {@link ClientRegistration client registration}. + * + * @return the {@link ClientRegistration} + */ + public ClientRegistration getClientRegistration() { + return this.clientRegistration; + } + + /** + * Returns the {@link OAuth2AuthorizationExchange authorization exchange}. + * + * @return the {@link OAuth2AuthorizationExchange} + */ + public OAuth2AuthorizationExchange getAuthorizationExchange() { + return this.authorizationExchange; + } + + /** + * Returns the {@link OAuth2AccessToken access token}. + * + * @return the {@link OAuth2AccessToken} + */ + public OAuth2AccessToken getAccessToken() { + return this.accessToken; + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationExchangeValidator.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationExchangeValidator.java new file mode 100644 index 0000000000..a23b09f291 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationExchangeValidator.java @@ -0,0 +1,54 @@ +/* + * 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.authentication; + +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; + +/** + * A validator for an "exchange" of an OAuth 2.0 Authorization Request and Response. + * + * @author Joe Grandja + * @since 5.1 + * @see OAuth2AuthorizationExchange + */ +final class OAuth2AuthorizationExchangeValidator { + private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter"; + private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_parameter"; + + static void validate(OAuth2AuthorizationExchange authorizationExchange) { + OAuth2AuthorizationRequest authorizationRequest = authorizationExchange.getAuthorizationRequest(); + OAuth2AuthorizationResponse authorizationResponse = authorizationExchange.getAuthorizationResponse(); + + if (authorizationResponse.statusError()) { + throw new OAuth2AuthenticationException( + authorizationResponse.getError(), authorizationResponse.getError().toString()); + } + + if (!authorizationResponse.getState().equals(authorizationRequest.getState())) { + OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + } + + if (!authorizationResponse.getRedirectUri().equals(authorizationRequest.getRedirectUri())) { + OAuth2Error oauth2Error = new OAuth2Error(INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + } + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java index ab453f7c59..d3c442f334 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java @@ -25,11 +25,7 @@ import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCo import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; import org.springframework.security.oauth2.core.OAuth2AccessToken; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; -import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; -import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.util.Assert; @@ -60,8 +56,6 @@ import java.util.Collection; * @see Section 4.1.4 Access Token Response */ public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider { - private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter"; - private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_parameter"; private final OAuth2AccessTokenResponseClient accessTokenResponseClient; private final OAuth2UserService userService; private GrantedAuthoritiesMapper authoritiesMapper = (authorities -> authorities); @@ -97,25 +91,8 @@ public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider return null; } - OAuth2AuthorizationRequest authorizationRequest = authorizationCodeAuthentication - .getAuthorizationExchange().getAuthorizationRequest(); - OAuth2AuthorizationResponse authorizationResponse = authorizationCodeAuthentication - .getAuthorizationExchange().getAuthorizationResponse(); - - if (authorizationResponse.statusError()) { - throw new OAuth2AuthenticationException( - authorizationResponse.getError(), authorizationResponse.getError().toString()); - } - - if (!authorizationResponse.getState().equals(authorizationRequest.getState())) { - OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE); - throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); - } - - if (!authorizationResponse.getRedirectUri().equals(authorizationRequest.getRedirectUri())) { - OAuth2Error oauth2Error = new OAuth2Error(INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE); - throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); - } + OAuth2AuthorizationExchangeValidator.validate( + authorizationCodeAuthentication.getAuthorizationExchange()); OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenResponseClient.getTokenResponse( diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java new file mode 100644 index 0000000000..ebabb792a6 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java @@ -0,0 +1,209 @@ +/* + * 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; + +import org.springframework.security.authentication.AuthenticationDetailsSource; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationProvider; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; +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.DefaultRedirectStrategy; +import org.springframework.security.web.RedirectStrategy; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.security.web.savedrequest.HttpSessionRequestCache; +import org.springframework.security.web.savedrequest.RequestCache; +import org.springframework.security.web.savedrequest.SavedRequest; +import org.springframework.security.web.util.UrlUtils; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 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: + * + *

    + *
  • + * 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). + *
  • + *
  • + * This {@code Filter} will then create an {@link OAuth2AuthorizationCodeAuthenticationToken} with + * the {@link OAuth2ParameterNames#CODE code} received and + * delegate it to the {@link AuthenticationManager} to authenticate. + *
  • + *
  • + * 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 OAuth2AuthorizedClientService}. + *
  • + *
+ * + * @author Joe Grandja + * @since 5.1 + * @see OAuth2AuthorizationCodeAuthenticationToken + * @see OAuth2AuthorizationCodeAuthenticationProvider + * @see OAuth2AuthorizationRequest + * @see OAuth2AuthorizationResponse + * @see AuthorizationRequestRepository + * @see OAuth2AuthorizationRequestRedirectFilter + * @see ClientRegistrationRepository + * @see OAuth2AuthorizedClient + * @see OAuth2AuthorizedClientService + * @see Section 4.1 Authorization Code Grant + * @see Section 4.1.2 Authorization Response + */ +public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter { + private final ClientRegistrationRepository clientRegistrationRepository; + private final OAuth2AuthorizedClientService authorizedClientService; + private final AuthenticationManager authenticationManager; + private AuthorizationRequestRepository authorizationRequestRepository = + new HttpSessionOAuth2AuthorizationRequestRepository(); + private final AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource(); + private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); + private final RequestCache requestCache = new HttpSessionRequestCache(); + + /** + * Constructs an {@code OAuth2AuthorizationCodeGrantFilter} using the provided parameters. + * + * @param clientRegistrationRepository the repository of client registrations + * @param authorizedClientService the authorized client service + * @param authenticationManager the authentication manager + */ + public OAuth2AuthorizationCodeGrantFilter(ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientService authorizedClientService, + AuthenticationManager authenticationManager) { + Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); + Assert.notNull(authorizedClientService, "authorizedClientService cannot be null"); + Assert.notNull(authenticationManager, "authenticationManager cannot be null"); + this.clientRegistrationRepository = clientRegistrationRepository; + this.authorizedClientService = authorizedClientService; + this.authenticationManager = authenticationManager; + } + + /** + * Sets the repository for stored {@link OAuth2AuthorizationRequest}'s. + * + * @param authorizationRequestRepository the repository for stored {@link OAuth2AuthorizationRequest}'s + */ + public final void setAuthorizationRequestRepository(AuthorizationRequestRepository authorizationRequestRepository) { + Assert.notNull(authorizationRequestRepository, "authorizationRequestRepository cannot be null"); + this.authorizationRequestRepository = authorizationRequestRepository; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + if (this.shouldProcessAuthorizationResponse(request)) { + this.processAuthorizationResponse(request, response); + return; + } + + filterChain.doFilter(request, response); + } + + private boolean shouldProcessAuthorizationResponse(HttpServletRequest request) { + OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.loadAuthorizationRequest(request); + if (authorizationRequest == null) { + return false; + } + String requestUrl = UrlUtils.buildFullRequestUrl(request.getScheme(), request.getServerName(), + request.getServerPort(), request.getRequestURI(), null); + if (requestUrl.equals(authorizationRequest.getRedirectUri()) && + OAuth2AuthorizationResponseUtils.isAuthorizationResponse(request)) { + return true; + } + return false; + } + + private void processAuthorizationResponse(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.removeAuthorizationRequest(request); + + String registrationId = (String) authorizationRequest.getAdditionalParameters().get(OAuth2ParameterNames.REGISTRATION_ID); + ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId); + + OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(request); + + OAuth2AuthorizationCodeAuthenticationToken authenticationRequest = new OAuth2AuthorizationCodeAuthenticationToken( + clientRegistration, new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse)); + authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); + + OAuth2AuthorizationCodeAuthenticationToken authenticationResult; + + try { + authenticationResult = (OAuth2AuthorizationCodeAuthenticationToken) + this.authenticationManager.authenticate(authenticationRequest); + } catch (OAuth2AuthenticationException ex) { + OAuth2Error error = ex.getError(); + UriComponentsBuilder uriBuilder = UriComponentsBuilder + .fromUriString(authorizationResponse.getRedirectUri()) + .queryParam(OAuth2ParameterNames.ERROR, error.getErrorCode()); + if (!StringUtils.isEmpty(error.getDescription())) { + uriBuilder.queryParam(OAuth2ParameterNames.ERROR_DESCRIPTION, error.getDescription()); + } + if (!StringUtils.isEmpty(error.getUri())) { + uriBuilder.queryParam(OAuth2ParameterNames.ERROR_URI, error.getUri()); + } + this.redirectStrategy.sendRedirect(request, response, uriBuilder.build().encode().toString()); + return; + } + + Authentication currentAuthentication = SecurityContextHolder.getContext().getAuthentication(); + + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + authenticationResult.getClientRegistration(), + currentAuthentication.getName(), + authenticationResult.getAccessToken()); + + this.authorizedClientService.saveAuthorizedClient(authorizedClient, currentAuthentication); + + String redirectUrl = authorizationResponse.getRedirectUri(); + SavedRequest savedRequest = this.requestCache.getRequest(request, response); + if (savedRequest != null) { + redirectUrl = savedRequest.getRedirectUrl(); + this.requestCache.removeRequest(request, response); + } + + this.redirectStrategy.sendRedirect(request, response, redirectUrl); + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilter.java index 4fea8b7acb..89785c76d6 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * 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. @@ -18,6 +18,7 @@ package org.springframework.security.oauth2.client.web; import org.springframework.http.HttpStatus; import org.springframework.security.crypto.keygen.Base64StringKeyGenerator; import org.springframework.security.crypto.keygen.StringKeyGenerator; +import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.AuthorizationGrantType; @@ -25,6 +26,9 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.web.DefaultRedirectStrategy; import org.springframework.security.web.RedirectStrategy; +import org.springframework.security.web.savedrequest.HttpSessionRequestCache; +import org.springframework.security.web.savedrequest.RequestCache; +import org.springframework.security.web.util.ThrowableAnalyzer; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.util.Assert; import org.springframework.web.filter.OncePerRequestFilter; @@ -51,6 +55,17 @@ import java.util.Map; * response type, and a redirection URI which the authorization server will send the user-agent back to * once access is granted (or denied) by the End-User (Resource Owner). * + *

+ * By default, this {@code Filter} responds to authorization requests + * at the {@code URI} {@code /oauth2/authorization/{registrationId}}. + * The {@code URI} template variable {@code {registrationId}} represents the + * {@link ClientRegistration#getRegistrationId() registration identifier} of the client + * that is used for initiating the OAuth 2.0 Authorization Request. + * + *

+ * NOTE: The default base {@code URI} {@code /oauth2/authorization} may be overridden + * via it's constructor {@link #OAuth2AuthorizationRequestRedirectFilter(ClientRegistrationRepository, String)}. + * @author Joe Grandja * @author Rob Winch * @since 5.0 @@ -69,6 +84,8 @@ public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilt */ public static final String DEFAULT_AUTHORIZATION_REQUEST_BASE_URI = "/oauth2/authorization"; private static final String REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId"; + private static final String AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME = + ClientAuthorizationRequiredException.class.getName() + ".AUTHORIZATION_REQUIRED_EXCEPTION"; private final AntPathRequestMatcher authorizationRequestMatcher; private final ClientRegistrationRepository clientRegistrationRepository; private final OAuth2AuthorizationRequestUriBuilder authorizationRequestUriBuilder = new OAuth2AuthorizationRequestUriBuilder(); @@ -76,6 +93,8 @@ public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilt private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder()); private AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionOAuth2AuthorizationRequestRepository(); + private final ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer(); + private final RequestCache requestCache = new HttpSessionRequestCache(); /** * Constructs an {@code OAuth2AuthorizationRequestRedirectFilter} using the provided parameters. @@ -125,7 +144,36 @@ public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilt return; } - filterChain.doFilter(request, response); + try { + filterChain.doFilter(request, response); + } catch (IOException ex) { + throw ex; + } catch (Exception ex) { + // Check to see if we need to handle ClientAuthorizationRequiredException + Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex); + ClientAuthorizationRequiredException authzEx = (ClientAuthorizationRequiredException) this.throwableAnalyzer + .getFirstThrowableOfType(ClientAuthorizationRequiredException.class, causeChain); + if (authzEx != null) { + try { + request.setAttribute(AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME, authzEx); + this.sendRedirectForAuthorization(request, response, authzEx.getClientRegistrationId()); + this.requestCache.saveRequest(request, response); + } catch (Exception failed) { + this.unsuccessfulRedirectForAuthorization(request, response, failed); + } finally { + request.removeAttribute(AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME); + } + return; + } + + if (ex instanceof ServletException) { + throw (ServletException) ex; + } else if (ex instanceof RuntimeException) { + throw (RuntimeException) ex; + } else { + throw new RuntimeException(ex); + } + } } private boolean shouldRequestAuthorization(HttpServletRequest request, HttpServletResponse response) { @@ -133,14 +181,25 @@ public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilt } private void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response) - throws IOException, ServletException { + throws IOException, ServletException { String registrationId = this.authorizationRequestMatcher .extractUriTemplateVariables(request).get(REGISTRATION_ID_URI_VARIABLE_NAME); + this.sendRedirectForAuthorization(request, response, registrationId); + } + + private void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response, + String registrationId) throws IOException, ServletException { + ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId); if (clientRegistration == null) { throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId); } + this.sendRedirectForAuthorization(request, response, clientRegistration); + } + + private void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response, + ClientRegistration clientRegistration) throws IOException, ServletException { String redirectUriStr = this.expandRedirectUri(request, clientRegistration); @@ -188,6 +247,11 @@ public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilt port = -1; // Removes the port in UriComponentsBuilder } + // Supported URI variables -> baseUrl, action, registrationId + // Used in -> CommonOAuth2Provider.DEFAULT_REDIRECT_URL = "{baseUrl}/{action}/oauth2/code/{registrationId}" + Map uriVariables = new HashMap<>(); + uriVariables.put("registrationId", clientRegistration.getRegistrationId()); + String baseUrl = UriComponentsBuilder.newInstance() .scheme(request.getScheme()) .host(request.getServerName()) @@ -195,13 +259,40 @@ public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilt .path(request.getContextPath()) .build() .toUriString(); - - Map uriVariables = new HashMap<>(); uriVariables.put("baseUrl", baseUrl); - uriVariables.put("registrationId", clientRegistration.getRegistrationId()); + + if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) { + String loginAction = "login"; + String authorizeAction = "authorize"; + String actionParameter = "action"; + String action; + if (request.getAttribute(AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME) != null) { + action = authorizeAction; + } else if (request.getParameter(actionParameter) == null) { + action = loginAction; + } else { + String actionValue = request.getParameter(actionParameter); + if (loginAction.equalsIgnoreCase(actionValue)) { + action = loginAction; + } else { + action = authorizeAction; + } + } + uriVariables.put("action", action); + } return UriComponentsBuilder.fromUriString(clientRegistration.getRedirectUriTemplate()) .buildAndExpand(uriVariables) .toUriString(); } + + private static final class DefaultThrowableAnalyzer extends ThrowableAnalyzer { + protected void initExtractorMap() { + super.initExtractorMap(); + registerExtractor(ServletException.class, throwable -> { + ThrowableAnalyzer.verifyThrowableHierarchy(throwable, ServletException.class); + return ((ServletException) throwable).getRootCause(); + }); + } + } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationResponseUtils.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationResponseUtils.java new file mode 100644 index 0000000000..9668f68536 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationResponseUtils.java @@ -0,0 +1,72 @@ +/* + * 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; + +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.util.StringUtils; + +import javax.servlet.http.HttpServletRequest; + +/** + * Utility methods for an OAuth 2.0 Authorization Response. + * + * @author Joe Grandja + * @since 5.1 + * @see OAuth2AuthorizationResponse + */ +final class OAuth2AuthorizationResponseUtils { + + private OAuth2AuthorizationResponseUtils() { + } + + static boolean isAuthorizationResponse(HttpServletRequest request) { + return isAuthorizationResponseSuccess(request) || isAuthorizationResponseError(request); + } + + static boolean isAuthorizationResponseSuccess(HttpServletRequest request) { + return StringUtils.hasText(request.getParameter(OAuth2ParameterNames.CODE)) && + StringUtils.hasText(request.getParameter(OAuth2ParameterNames.STATE)); + } + + static boolean isAuthorizationResponseError(HttpServletRequest request) { + return StringUtils.hasText(request.getParameter(OAuth2ParameterNames.ERROR)) && + StringUtils.hasText(request.getParameter(OAuth2ParameterNames.STATE)); + } + + static OAuth2AuthorizationResponse convert(HttpServletRequest request) { + String code = request.getParameter(OAuth2ParameterNames.CODE); + String errorCode = request.getParameter(OAuth2ParameterNames.ERROR); + String state = request.getParameter(OAuth2ParameterNames.STATE); + String redirectUri = request.getRequestURL().toString(); + + if (StringUtils.hasText(code)) { + return OAuth2AuthorizationResponse.success(code) + .redirectUri(redirectUri) + .state(state) + .build(); + } else { + String errorDescription = request.getParameter(OAuth2ParameterNames.ERROR_DESCRIPTION); + String errorUri = request.getParameter(OAuth2ParameterNames.ERROR_URI); + return OAuth2AuthorizationResponse.error(errorCode) + .redirectUri(redirectUri) + .errorDescription(errorDescription) + .errorUri(errorUri) + .state(state) + .build(); + } + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java index 67da59488e..fb39896d10 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java @@ -35,7 +35,6 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.util.Assert; -import org.springframework.util.StringUtils; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -134,22 +133,21 @@ public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProce public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { - if (!this.authorizationResponseSuccess(request) && !this.authorizationResponseError(request)) { + if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(request)) { OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); } - OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.loadAuthorizationRequest(request); + OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.removeAuthorizationRequest(request); if (authorizationRequest == null) { OAuth2Error oauth2Error = new OAuth2Error(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); } - this.authorizationRequestRepository.removeAuthorizationRequest(request); String registrationId = (String) authorizationRequest.getAdditionalParameters().get(OAuth2ParameterNames.REGISTRATION_ID); ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId); - OAuth2AuthorizationResponse authorizationResponse = this.convert(request); + OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(request); OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken( clientRegistration, new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse)); @@ -182,37 +180,4 @@ public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProce Assert.notNull(authorizationRequestRepository, "authorizationRequestRepository cannot be null"); this.authorizationRequestRepository = authorizationRequestRepository; } - - private OAuth2AuthorizationResponse convert(HttpServletRequest request) { - String code = request.getParameter(OAuth2ParameterNames.CODE); - String errorCode = request.getParameter(OAuth2ParameterNames.ERROR); - String state = request.getParameter(OAuth2ParameterNames.STATE); - String redirectUri = request.getRequestURL().toString(); - - if (StringUtils.hasText(code)) { - return OAuth2AuthorizationResponse.success(code) - .redirectUri(redirectUri) - .state(state) - .build(); - } else { - String errorDescription = request.getParameter(OAuth2ParameterNames.ERROR_DESCRIPTION); - String errorUri = request.getParameter(OAuth2ParameterNames.ERROR_URI); - return OAuth2AuthorizationResponse.error(errorCode) - .redirectUri(redirectUri) - .errorDescription(errorDescription) - .errorUri(errorUri) - .state(state) - .build(); - } - } - - private boolean authorizationResponseSuccess(HttpServletRequest request) { - return StringUtils.hasText(request.getParameter(OAuth2ParameterNames.CODE)) && - StringUtils.hasText(request.getParameter(OAuth2ParameterNames.STATE)); - } - - private boolean authorizationResponseError(HttpServletRequest request) { - return StringUtils.hasText(request.getParameter(OAuth2ParameterNames.ERROR)) && - StringUtils.hasText(request.getParameter(OAuth2ParameterNames.STATE)); - } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java new file mode 100644 index 0000000000..ba88b9f570 --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java @@ -0,0 +1,141 @@ +/* + * 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.authentication; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +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.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link OAuth2AuthorizationCodeAuthenticationProvider}. + * + * @author Joe Grandja + */ +@PrepareForTest({ClientRegistration.class, OAuth2AuthorizationRequest.class, + OAuth2AuthorizationResponse.class, OAuth2AccessTokenResponse.class}) +@RunWith(PowerMockRunner.class) +public class OAuth2AuthorizationCodeAuthenticationProviderTests { + private ClientRegistration clientRegistration; + private OAuth2AuthorizationRequest authorizationRequest; + private OAuth2AuthorizationResponse authorizationResponse; + private OAuth2AuthorizationExchange authorizationExchange; + private OAuth2AccessTokenResponseClient accessTokenResponseClient; + private OAuth2AuthorizationCodeAuthenticationProvider authenticationProvider; + + @Before + @SuppressWarnings("unchecked") + public void setUp() throws Exception { + this.clientRegistration = mock(ClientRegistration.class); + this.authorizationRequest = mock(OAuth2AuthorizationRequest.class); + this.authorizationResponse = mock(OAuth2AuthorizationResponse.class); + this.authorizationExchange = new OAuth2AuthorizationExchange(this.authorizationRequest, this.authorizationResponse); + this.accessTokenResponseClient = mock(OAuth2AccessTokenResponseClient.class); + this.authenticationProvider = new OAuth2AuthorizationCodeAuthenticationProvider(this.accessTokenResponseClient); + + when(this.authorizationRequest.getState()).thenReturn("12345"); + when(this.authorizationResponse.getState()).thenReturn("12345"); + when(this.authorizationRequest.getRedirectUri()).thenReturn("http://example.com"); + when(this.authorizationResponse.getRedirectUri()).thenReturn("http://example.com"); + } + + @Test + public void constructorWhenAccessTokenResponseClientIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OAuth2AuthorizationCodeAuthenticationProvider(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void supportsWhenTypeOAuth2AuthorizationCodeAuthenticationTokenThenReturnTrue() { + assertThat(this.authenticationProvider.supports(OAuth2AuthorizationCodeAuthenticationToken.class)).isTrue(); + } + + @Test + public void authenticateWhenAuthorizationErrorResponseThenThrowOAuth2AuthenticationException() { + when(this.authorizationResponse.statusError()).thenReturn(true); + when(this.authorizationResponse.getError()).thenReturn(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST)); + + assertThatThrownBy(() -> { + this.authenticationProvider.authenticate( + new OAuth2AuthorizationCodeAuthenticationToken( + this.clientRegistration, this.authorizationExchange)); + }).isInstanceOf(OAuth2AuthenticationException.class).hasMessageContaining(OAuth2ErrorCodes.INVALID_REQUEST); + } + + @Test + public void authenticateWhenAuthorizationResponseStateNotEqualAuthorizationRequestStateThenThrowOAuth2AuthenticationException() { + when(this.authorizationRequest.getState()).thenReturn("12345"); + when(this.authorizationResponse.getState()).thenReturn("67890"); + + assertThatThrownBy(() -> { + this.authenticationProvider.authenticate( + new OAuth2AuthorizationCodeAuthenticationToken( + this.clientRegistration, this.authorizationExchange)); + }).isInstanceOf(OAuth2AuthenticationException.class).hasMessageContaining("invalid_state_parameter"); + } + + @Test + public void authenticateWhenAuthorizationResponseRedirectUriNotEqualAuthorizationRequestRedirectUriThenThrowOAuth2AuthenticationException() { + when(this.authorizationRequest.getRedirectUri()).thenReturn("http://example.com"); + when(this.authorizationResponse.getRedirectUri()).thenReturn("http://example2.com"); + + assertThatThrownBy(() -> { + this.authenticationProvider.authenticate( + new OAuth2AuthorizationCodeAuthenticationToken( + this.clientRegistration, this.authorizationExchange)); + }).isInstanceOf(OAuth2AuthenticationException.class).hasMessageContaining("invalid_redirect_uri_parameter"); + } + + @Test + public void authenticateWhenAuthorizationSuccessResponseThenExchangedForAccessToken() { + OAuth2AccessToken accessToken = mock(OAuth2AccessToken.class); + OAuth2AccessTokenResponse accessTokenResponse = mock(OAuth2AccessTokenResponse.class); + when(accessTokenResponse.getAccessToken()).thenReturn(accessToken); + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse); + + OAuth2AuthorizationCodeAuthenticationToken authenticationResult = + (OAuth2AuthorizationCodeAuthenticationToken) this.authenticationProvider.authenticate( + new OAuth2AuthorizationCodeAuthenticationToken(this.clientRegistration, this.authorizationExchange)); + + assertThat(authenticationResult.isAuthenticated()).isTrue(); + assertThat(authenticationResult.getPrincipal()).isEqualTo(this.clientRegistration.getClientId()); + assertThat(authenticationResult.getCredentials()).isEqualTo(accessToken.getTokenValue()); + assertThat(authenticationResult.getAuthorities()).isEqualTo(Collections.emptyList()); + assertThat(authenticationResult.getClientRegistration()).isEqualTo(this.clientRegistration); + assertThat(authenticationResult.getAuthorizationExchange()).isEqualTo(this.authorizationExchange); + assertThat(authenticationResult.getAccessToken()).isEqualTo(accessToken); + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationTokenTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationTokenTests.java new file mode 100644 index 0000000000..0017a46c91 --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationTokenTests.java @@ -0,0 +1,115 @@ +/* + * 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.authentication; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link OAuth2AuthorizationCodeAuthenticationToken}. + * + * @author Joe Grandja + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest({ClientRegistration.class, OAuth2AuthorizationExchange.class, OAuth2AuthorizationResponse.class}) +public class OAuth2AuthorizationCodeAuthenticationTokenTests { + private ClientRegistration clientRegistration; + private OAuth2AuthorizationExchange authorizationExchange; + private OAuth2AccessToken accessToken; + + @Before + public void setUp() { + this.clientRegistration = mock(ClientRegistration.class); + this.authorizationExchange = mock(OAuth2AuthorizationExchange.class); + this.accessToken = mock(OAuth2AccessToken.class); + } + + @Test + public void constructorAuthorizationRequestResponseWhenClientRegistrationIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OAuth2AuthorizationCodeAuthenticationToken(null, this.authorizationExchange)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void constructorAuthorizationRequestResponseWhenAuthorizationExchangeIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OAuth2AuthorizationCodeAuthenticationToken(this.clientRegistration, null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void constructorAuthorizationRequestResponseWhenAllParametersProvidedAndValidThenCreated() { + OAuth2AuthorizationResponse authorizationResponse = mock(OAuth2AuthorizationResponse.class); + when(authorizationResponse.getCode()).thenReturn("code"); + when(this.authorizationExchange.getAuthorizationResponse()).thenReturn(authorizationResponse); + + OAuth2AuthorizationCodeAuthenticationToken authentication = + new OAuth2AuthorizationCodeAuthenticationToken(this.clientRegistration, this.authorizationExchange); + + assertThat(authentication.getPrincipal()).isEqualTo(this.clientRegistration.getClientId()); + assertThat(authentication.getCredentials()).isEqualTo(this.authorizationExchange.getAuthorizationResponse().getCode()); + assertThat(authentication.getAuthorities()).isEqualTo(Collections.emptyList()); + assertThat(authentication.getClientRegistration()).isEqualTo(this.clientRegistration); + assertThat(authentication.getAuthorizationExchange()).isEqualTo(this.authorizationExchange); + assertThat(authentication.getAccessToken()).isNull(); + assertThat(authentication.isAuthenticated()).isEqualTo(false); + } + + @Test + public void constructorTokenRequestResponseWhenClientRegistrationIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OAuth2AuthorizationCodeAuthenticationToken(null, this.authorizationExchange, this.accessToken)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void constructorTokenRequestResponseWhenAuthorizationExchangeIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OAuth2AuthorizationCodeAuthenticationToken(this.clientRegistration, null, this.accessToken)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void constructorTokenRequestResponseWhenAccessTokenIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OAuth2AuthorizationCodeAuthenticationToken(this.clientRegistration, this.authorizationExchange, null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void constructorTokenRequestResponseWhenAllParametersProvidedAndValidThenCreated() { + OAuth2AuthorizationCodeAuthenticationToken authentication = new OAuth2AuthorizationCodeAuthenticationToken( + this.clientRegistration, this.authorizationExchange, this.accessToken); + + assertThat(authentication.getPrincipal()).isEqualTo(this.clientRegistration.getClientId()); + assertThat(authentication.getCredentials()).isEqualTo(this.accessToken.getTokenValue()); + assertThat(authentication.getAuthorities()).isEqualTo(Collections.emptyList()); + assertThat(authentication.getClientRegistration()).isEqualTo(this.clientRegistration); + assertThat(authentication.getAuthorizationExchange()).isEqualTo(this.authorizationExchange); + assertThat(authentication.getAccessToken()).isEqualTo(this.accessToken); + assertThat(authentication.isAuthenticated()).isEqualTo(true); + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilterTests.java new file mode 100644 index 0000000000..6678d6e8b9 --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilterTests.java @@ -0,0 +1,304 @@ +/* + * 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; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +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.OAuth2AuthorizationExchange; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.web.savedrequest.HttpSessionRequestCache; +import org.springframework.security.web.savedrequest.RequestCache; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +/** + * Tests for {@link OAuth2AuthorizationCodeGrantFilter}. + * + * @author Joe Grandja + */ +@PowerMockIgnore("javax.security.*") +@PrepareForTest({OAuth2AuthorizationRequest.class, OAuth2AuthorizationExchange.class, OAuth2AuthorizationCodeGrantFilter.class}) +@RunWith(PowerMockRunner.class) +public class OAuth2AuthorizationCodeGrantFilterTests { + private ClientRegistration registration1; + private String principalName1 = "principal-1"; + private ClientRegistrationRepository clientRegistrationRepository; + private OAuth2AuthorizedClientService authorizedClientService; + private AuthenticationManager authenticationManager; + private AuthorizationRequestRepository authorizationRequestRepository; + private OAuth2AuthorizationCodeGrantFilter filter; + + @Before + public void setUp() { + this.registration1 = ClientRegistration.withRegistrationId("registration-1") + .clientId("client-1") + .clientSecret("secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate("{baseUrl}/callback/client-1") + .scope("user") + .authorizationUri("https://provider.com/oauth2/authorize") + .tokenUri("https://provider.com/oauth2/token") + .userInfoUri("https://provider.com/oauth2/user") + .userNameAttributeName("id") + .clientName("client-1") + .build(); + this.clientRegistrationRepository = new InMemoryClientRegistrationRepository(this.registration1); + this.authorizedClientService = new InMemoryOAuth2AuthorizedClientService(this.clientRegistrationRepository); + this.authorizationRequestRepository = new HttpSessionOAuth2AuthorizationRequestRepository(); + this.authenticationManager = mock(AuthenticationManager.class); + this.filter = spy(new OAuth2AuthorizationCodeGrantFilter( + this.clientRegistrationRepository, this.authorizedClientService, this.authenticationManager)); + this.filter.setAuthorizationRequestRepository(this.authorizationRequestRepository); + + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(new TestingAuthenticationToken(this.principalName1, "password")); + SecurityContextHolder.setContext(securityContext); + } + + @Test + public void constructorWhenClientRegistrationRepositoryIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OAuth2AuthorizationCodeGrantFilter(null, this.authorizedClientService, this.authenticationManager)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void constructorWhenAuthorizedClientServiceIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OAuth2AuthorizationCodeGrantFilter(this.clientRegistrationRepository, null, this.authenticationManager)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void constructorWhenAuthenticationManagerIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> new OAuth2AuthorizationCodeGrantFilter(this.clientRegistrationRepository, this.authorizedClientService, null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void setAuthorizationRequestRepositoryWhenAuthorizationRequestRepositoryIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.filter.setAuthorizationRequestRepository(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void doFilterWhenNotAuthorizationResponseThenNotProcessed() throws Exception { + String requestUri = "/path"; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + // NOTE: A valid Authorization Response contains either a 'code' or 'error' parameter. + + HttpServletResponse response = mock(HttpServletResponse.class); + FilterChain filterChain = mock(FilterChain.class); + + this.filter.doFilter(request, response, filterChain); + + verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + } + + @Test + public void doFilterWhenAuthorizationRequestNotFoundThenNotProcessed() throws Exception { + String requestUri = "/path"; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + request.addParameter(OAuth2ParameterNames.CODE, "code"); + request.addParameter(OAuth2ParameterNames.STATE, "state"); + + HttpServletResponse response = mock(HttpServletResponse.class); + FilterChain filterChain = mock(FilterChain.class); + + this.filter.doFilter(request, response, filterChain); + + verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + } + + @Test + public void doFilterWhenAuthorizationResponseUrlDoesNotMatchAuthorizationRequestRedirectUriThenNotProcessed() throws Exception { + String requestUri = "/callback/client-1"; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + request.addParameter(OAuth2ParameterNames.CODE, "code"); + request.addParameter(OAuth2ParameterNames.STATE, "state"); + + HttpServletResponse response = mock(HttpServletResponse.class); + FilterChain filterChain = mock(FilterChain.class); + + this.setUpAuthorizationRequest(request, response, this.registration1); + request.setRequestURI(requestUri + "-no-match"); + + this.filter.doFilter(request, response, filterChain); + + verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + } + + @Test + public void doFilterWhenAuthorizationResponseValidThenAuthorizationRequestRemoved() throws Exception { + String requestUri = "/callback/client-1"; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + request.addParameter(OAuth2ParameterNames.CODE, "code"); + request.addParameter(OAuth2ParameterNames.STATE, "state"); + + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + this.setUpAuthorizationRequest(request, response, this.registration1); + this.setUpAuthenticationResult(this.registration1); + + this.filter.doFilter(request, response, filterChain); + + assertThat(this.authorizationRequestRepository.loadAuthorizationRequest(request)).isNull(); + } + + @Test + public void doFilterWhenAuthenticationFailsThenHandleOAuth2AuthenticationException() throws Exception { + String requestUri = "/callback/client-1"; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + request.addParameter(OAuth2ParameterNames.CODE, "code"); + request.addParameter(OAuth2ParameterNames.STATE, "state"); + + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + this.setUpAuthorizationRequest(request, response, this.registration1); + OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT); + when(this.authenticationManager.authenticate(any(Authentication.class))) + .thenThrow(new OAuth2AuthenticationException(error, error.toString())); + + this.filter.doFilter(request, response, filterChain); + + assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/callback/client-1?error=invalid_grant"); + } + + @Test + public void doFilterWhenAuthorizationResponseSuccessThenAuthorizedClientSaved() throws Exception { + String requestUri = "/callback/client-1"; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + request.addParameter(OAuth2ParameterNames.CODE, "code"); + request.addParameter(OAuth2ParameterNames.STATE, "state"); + + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + this.setUpAuthorizationRequest(request, response, this.registration1); + this.setUpAuthenticationResult(this.registration1); + + this.filter.doFilter(request, response, filterChain); + + OAuth2AuthorizedClient authorizedClient = this.authorizedClientService.loadAuthorizedClient( + this.registration1.getRegistrationId(), this.principalName1); + assertThat(authorizedClient).isNotNull(); + assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration1); + assertThat(authorizedClient.getPrincipalName()).isEqualTo(this.principalName1); + assertThat(authorizedClient.getAccessToken()).isNotNull(); + } + + @Test + public void doFilterWhenAuthorizationResponseSuccessThenRedirected() throws Exception { + String requestUri = "/callback/client-1"; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + request.addParameter(OAuth2ParameterNames.CODE, "code"); + request.addParameter(OAuth2ParameterNames.STATE, "state"); + + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + this.setUpAuthorizationRequest(request, response, this.registration1); + this.setUpAuthenticationResult(this.registration1); + + this.filter.doFilter(request, response, filterChain); + + assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/callback/client-1"); + } + + @Test + public void doFilterWhenAuthorizationResponseSuccessHasSavedRequestThenRedirectedToSavedRequest() throws Exception { + String requestUri = "/saved-request"; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + MockHttpServletResponse response = new MockHttpServletResponse(); + RequestCache requestCache = new HttpSessionRequestCache(); + requestCache.saveRequest(request, response); + + requestUri = "/callback/client-1"; + request.setRequestURI(requestUri); + request.addParameter(OAuth2ParameterNames.CODE, "code"); + request.addParameter(OAuth2ParameterNames.STATE, "state"); + + FilterChain filterChain = mock(FilterChain.class); + + this.setUpAuthorizationRequest(request, response, this.registration1); + this.setUpAuthenticationResult(this.registration1); + + this.filter.doFilter(request, response, filterChain); + + assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/saved-request"); + } + + private void setUpAuthorizationRequest(HttpServletRequest request, HttpServletResponse response, + ClientRegistration registration) { + Map additionalParameters = new HashMap<>(); + additionalParameters.put(OAuth2ParameterNames.REGISTRATION_ID, registration.getRegistrationId()); + OAuth2AuthorizationRequest authorizationRequest = mock(OAuth2AuthorizationRequest.class); + when(authorizationRequest.getAdditionalParameters()).thenReturn(additionalParameters); + when(authorizationRequest.getRedirectUri()).thenReturn(request.getRequestURL().toString()); + when(authorizationRequest.getState()).thenReturn("state"); + this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response); + } + + private void setUpAuthenticationResult(ClientRegistration registration) { + OAuth2AuthorizationCodeAuthenticationToken authentication = mock(OAuth2AuthorizationCodeAuthenticationToken.class); + when(authentication.getClientRegistration()).thenReturn(registration); + when(authentication.getAuthorizationExchange()).thenReturn(mock(OAuth2AuthorizationExchange.class)); + when(authentication.getAccessToken()).thenReturn(mock(OAuth2AccessToken.class)); + when(this.authenticationManager.authenticate(any(Authentication.class))).thenReturn(authentication); + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilterTests.java index e3e8b734e8..cf9fdf95fa 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilterTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilterTests.java @@ -21,18 +21,25 @@ import org.mockito.ArgumentCaptor; import org.springframework.http.HttpStatus; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.web.savedrequest.SavedRequest; import javax.servlet.FilterChain; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.util.Collections; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.*; /** @@ -54,7 +61,7 @@ public class OAuth2AuthorizationRequestRedirectFilterTests { .clientSecret("secret") .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}") + .redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}") .scope("user") .authorizationUri("https://provider.com/oauth2/authorize") .tokenUri("https://provider.com/oauth2/token") @@ -67,7 +74,7 @@ public class OAuth2AuthorizationRequestRedirectFilterTests { .clientSecret("secret") .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}") + .redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}") .scope("openid", "profile", "email") .authorizationUri("https://provider.com/oauth2/authorize") .tokenUri("https://provider.com/oauth2/token") @@ -78,7 +85,7 @@ public class OAuth2AuthorizationRequestRedirectFilterTests { this.registration3 = ClientRegistration.withRegistrationId("registration-3") .clientId("client-3") .authorizationGrantType(AuthorizationGrantType.IMPLICIT) - .redirectUriTemplate("{baseUrl}/login/oauth2/implicit/{registrationId}") + .redirectUriTemplate("{baseUrl}/authorize/oauth2/implicit/{registrationId}") .scope("openid", "profile", "email") .authorizationUri("https://provider.com/oauth2/authorize") .tokenUri("https://provider.com/oauth2/token") @@ -90,19 +97,22 @@ public class OAuth2AuthorizationRequestRedirectFilterTests { this.filter = new OAuth2AuthorizationRequestRedirectFilter(this.clientRegistrationRepository); } - @Test(expected = IllegalArgumentException.class) + @Test public void constructorWhenClientRegistrationRepositoryIsNullThenThrowIllegalArgumentException() { - new OAuth2AuthorizationRequestRedirectFilter(null); + assertThatThrownBy(() -> new OAuth2AuthorizationRequestRedirectFilter(null)) + .isInstanceOf(IllegalArgumentException.class); } - @Test(expected = IllegalArgumentException.class) + @Test public void constructorWhenAuthorizationRequestBaseUriIsNullThenThrowIllegalArgumentException() { - new OAuth2AuthorizationRequestRedirectFilter(this.clientRegistrationRepository, null); + assertThatThrownBy(() -> new OAuth2AuthorizationRequestRedirectFilter(this.clientRegistrationRepository, null)) + .isInstanceOf(IllegalArgumentException.class); } - @Test(expected = IllegalArgumentException.class) + @Test public void setAuthorizationRequestRepositoryWhenAuthorizationRequestRepositoryIsNullThenThrowIllegalArgumentException() { - this.filter.setAuthorizationRequestRepository(null); + assertThatThrownBy(() -> this.filter.setAuthorizationRequestRepository(null)) + .isInstanceOf(IllegalArgumentException.class); } @Test @@ -136,7 +146,7 @@ public class OAuth2AuthorizationRequestRedirectFilterTests { } @Test - public void doFilterWhenAuthorizationRequestAuthorizationCodeGrantThenRedirectForAuthorization() throws Exception { + public void doFilterWhenAuthorizationRequestOAuth2LoginThenRedirectForAuthorization() throws Exception { String requestUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/" + this.registration1.getRegistrationId(); MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); @@ -152,7 +162,7 @@ public class OAuth2AuthorizationRequestRedirectFilterTests { } @Test - public void doFilterWhenAuthorizationRequestAuthorizationCodeGrantThenAuthorizationRequestSaved() throws Exception { + public void doFilterWhenAuthorizationRequestOAuth2LoginThenAuthorizationRequestSaved() throws Exception { String requestUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/" + this.registration2.getRegistrationId(); MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); @@ -184,7 +194,7 @@ public class OAuth2AuthorizationRequestRedirectFilterTests { verifyZeroInteractions(filterChain); - assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=token&client_id=client-3&scope=openid%20profile%20email&state=.{15,}&redirect_uri=http://localhost/login/oauth2/implicit/registration-3"); + assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=token&client_id=client-3&scope=openid%20profile%20email&state=.{15,}&redirect_uri=http://localhost/authorize/oauth2/implicit/registration-3"); } @Test @@ -292,4 +302,101 @@ public class OAuth2AuthorizationRequestRedirectFilterTests { assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=https://example.com/login/oauth2/code/registration-1"); } + + @Test + public void doFilterWhenNotAuthorizationRequestAndClientAuthorizationRequiredExceptionThrownThenRedirectForAuthorization() throws Exception { + String requestUri = "/path"; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + doThrow(new ClientAuthorizationRequiredException(this.registration1.getRegistrationId())) + .when(filterChain).doFilter(any(ServletRequest.class), any(ServletResponse.class)); + + this.filter.doFilter(request, response, filterChain); + + verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + + assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http://localhost/authorize/oauth2/code/registration-1"); + + HttpSession session = request.getSession(false); + assertThat(session).isNotNull(); + boolean requestSaved = false; + for (String attrName : Collections.list(session.getAttributeNames())) { + if (SavedRequest.class.isAssignableFrom(session.getAttribute(attrName).getClass())) { + requestSaved = true; + break; + } + } + assertThat(requestSaved).isTrue(); + } + + @Test + public void doFilterWhenNotAuthorizationRequestAndClientAuthorizationRequiredExceptionThrownThenRedirectUriIsAuthorize() throws Exception { + String requestUri = "/path"; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + doThrow(new ClientAuthorizationRequiredException(this.registration1.getRegistrationId())) + .when(filterChain).doFilter(any(ServletRequest.class), any(ServletResponse.class)); + + this.filter.doFilter(request, response, filterChain); + + verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + + assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http://localhost/authorize/oauth2/code/registration-1"); + } + + @Test + public void doFilterWhenAuthorizationRequestOAuth2LoginThenRedirectUriIsLogin() throws Exception { + String requestUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + + "/" + this.registration2.getRegistrationId(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + this.filter.doFilter(request, response, filterChain); + + verifyZeroInteractions(filterChain); + + assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-2&scope=openid%20profile%20email&state=.{15,}&redirect_uri=http://localhost/login/oauth2/code/registration-2"); + } + + @Test + public void doFilterWhenAuthorizationRequestHasActionParameterAuthorizeThenRedirectUriIsAuthorize() throws Exception { + String requestUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + + "/" + this.registration1.getRegistrationId(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.addParameter("action", "authorize"); + request.setServletPath(requestUri); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + this.filter.doFilter(request, response, filterChain); + + verifyZeroInteractions(filterChain); + + assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http://localhost/authorize/oauth2/code/registration-1"); + } + + @Test + public void doFilterWhenAuthorizationRequestHasActionParameterLoginThenRedirectUriIsLogin() throws Exception { + String requestUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + + "/" + this.registration2.getRegistrationId(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.addParameter("action", "login"); + request.setServletPath(requestUri); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + this.filter.doFilter(request, response, filterChain); + + verifyZeroInteractions(filterChain); + + assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-2&scope=openid%20profile%20email&state=.{15,}&redirect_uri=http://localhost/login/oauth2/code/registration-2"); + } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilterTests.java index 94b78a6fbf..3c765fe6e4 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilterTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilterTests.java @@ -19,7 +19,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; -import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; @@ -54,9 +53,8 @@ import java.util.HashMap; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.fail; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.*; -import static org.powermock.api.mockito.PowerMockito.verifyPrivate; /** * Tests for {@link OAuth2LoginAuthenticationFilter}. @@ -118,24 +116,28 @@ public class OAuth2LoginAuthenticationFilterTests { this.filter.setAuthenticationManager(this.authenticationManager); } - @Test(expected = IllegalArgumentException.class) + @Test public void constructorWhenClientRegistrationRepositoryIsNullThenThrowIllegalArgumentException() { - new OAuth2LoginAuthenticationFilter(null, this.authorizedClientService); + assertThatThrownBy(() -> new OAuth2LoginAuthenticationFilter(null, this.authorizedClientService)) + .isInstanceOf(IllegalArgumentException.class); } - @Test(expected = IllegalArgumentException.class) + @Test public void constructorWhenAuthorizedClientServiceIsNullThenThrowIllegalArgumentException() { - new OAuth2LoginAuthenticationFilter(this.clientRegistrationRepository, null); + assertThatThrownBy(() -> new OAuth2LoginAuthenticationFilter(this.clientRegistrationRepository, null)) + .isInstanceOf(IllegalArgumentException.class); } - @Test(expected = IllegalArgumentException.class) + @Test public void constructorWhenFilterProcessesUrlIsNullThenThrowIllegalArgumentException() { - new OAuth2LoginAuthenticationFilter(this.clientRegistrationRepository, this.authorizedClientService, null); + assertThatThrownBy(() -> new OAuth2LoginAuthenticationFilter(this.clientRegistrationRepository, this.authorizedClientService, null)) + .isInstanceOf(IllegalArgumentException.class); } - @Test(expected = IllegalArgumentException.class) + @Test public void setAuthorizationRequestRepositoryWhenAuthorizationRequestRepositoryIsNullThenThrowIllegalArgumentException() { - this.filter.setAuthorizationRequestRepository(null); + assertThatThrownBy(() -> this.filter.setAuthorizationRequestRepository(null)) + .isInstanceOf(IllegalArgumentException.class); } @Test @@ -268,25 +270,6 @@ public class OAuth2LoginAuthenticationFilterTests { verify(this.filter).attemptAuthentication(any(HttpServletRequest.class), any(HttpServletResponse.class)); } - @Test - public void attemptAuthenticationWhenAuthorizationRequestIsNullThenAuthorizationResponseNotCreated() throws Exception { - OAuth2LoginAuthenticationFilter filter = PowerMockito.spy(new OAuth2LoginAuthenticationFilter( - this.clientRegistrationRepository, this.authorizedClientService)); - - MockHttpServletRequest request = new MockHttpServletRequest(); - request.addParameter(OAuth2ParameterNames.CODE, "code"); - request.addParameter(OAuth2ParameterNames.STATE, "state"); - - MockHttpServletResponse response = new MockHttpServletResponse(); - - try { - filter.attemptAuthentication(request, response); - fail(); - } catch (OAuth2AuthenticationException ex) { - verifyPrivate(filter, never()).invoke("convert", any(HttpServletRequest.class)); - } - } - private void setUpAuthorizationRequest(HttpServletRequest request, HttpServletResponse response, ClientRegistration registration, String state) { OAuth2AuthorizationRequest authorizationRequest = mock(OAuth2AuthorizationRequest.class); diff --git a/samples/boot/oauth2/authcodegrant/README.adoc b/samples/boot/oauth2/authcodegrant/README.adoc new file mode 100644 index 0000000000..36920acc46 --- /dev/null +++ b/samples/boot/oauth2/authcodegrant/README.adoc @@ -0,0 +1,64 @@ += OAuth 2.0 Authorization Code Grant Sample + +== GitHub Repositories + +This guide provides instructions on setting up the sample application, which leverages the OAuth 2.0 Authorization Code Grant, and displays a list of public GitHub repositories that are accessible to the authenticated user. + +This includes repositories owned by the authenticated user, repositories where the authenticated user is a collaborator, and repositories that the authenticated user has access to through an organization membership. + +The following sections provide detailed steps for setting up the sample and covers the following topics: + +* <> +* <> +* <> + +[[github-register-application]] +=== Register OAuth application + +To use GitHub's OAuth 2.0 authorization system, you must https://github.com/settings/applications/new[Register a new OAuth application]. + +When registering the OAuth application, ensure the *Authorization callback URL* is set to `http://localhost:8080/github-repos`. + +The Authorization callback URL (redirect URI) is the path in the application that the end-user's user-agent is redirected back to after they have authenticated with GitHub and have granted access to the OAuth application on the _Authorize application_ page. + +[[github-application-config]] +=== Configure application.yml + +Now that you have a new OAuth application with GitHub, you need to configure the sample to use the OAuth application for the _authorization code grant flow_. +To do so: + +. Go to `application.yml` and set the following configuration: ++ +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: <1> + github: <2> + client-id: github-client-id + client-secret: github-client-secret + scope: public_repo + redirect-uri-template: "{baseUrl}/github-repos" + client-name: GitHub Repositories +---- ++ +.OAuth Client properties +==== +<1> `spring.security.oauth2.client.registration` is the base property prefix for OAuth Client properties. +<2> Following the base property prefix is the ID for the `ClientRegistration`, which is github. +==== + +. Replace the values in the `client-id` and `client-secret` property with the OAuth 2.0 credentials you created earlier. + +[[github-boot-application]] +=== Boot up the application + +Launch the Spring Boot 2.0 sample and go to `http://localhost:8080`. +You are then redirected to the default _auto-generated_ form login page. +Log in using *'user'* (username) and *'password'* (password) and then you'll be redirected to GitHub for authentication. + +After authenticating with your GitHub credentials, the next page presented to you is "Authorize application". +This page will ask you to *Authorize* the application you created in the previous step. +Click _Authorize application_ to allow the OAuth application to access and display your public repository information. diff --git a/samples/boot/oauth2/authcodegrant/spring-security-samples-boot-oauth2-authcodegrant.gradle b/samples/boot/oauth2/authcodegrant/spring-security-samples-boot-oauth2-authcodegrant.gradle new file mode 100644 index 0000000000..572fe40331 --- /dev/null +++ b/samples/boot/oauth2/authcodegrant/spring-security-samples-boot-oauth2-authcodegrant.gradle @@ -0,0 +1,16 @@ +apply plugin: 'io.spring.convention.spring-sample-boot' + +ext['thymeleaf.version'] = '3.0.9.RELEASE' + +dependencies { + compile project(':spring-security-config') + compile project(':spring-security-oauth2-client') + compile 'org.springframework:spring-webflux' + compile 'org.springframework.boot:spring-boot-starter-thymeleaf' + compile 'org.springframework.boot:spring-boot-starter-web' + compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity4' + compile 'io.projectreactor.ipc:reactor-netty' + + testCompile project(':spring-security-test') + testCompile 'org.springframework.boot:spring-boot-starter-test' +} diff --git a/samples/boot/oauth2/authcodegrant/src/integration-test/java/org/springframework/security/samples/OAuth2AuthorizationCodeGrantApplicationTests.java b/samples/boot/oauth2/authcodegrant/src/integration-test/java/org/springframework/security/samples/OAuth2AuthorizationCodeGrantApplicationTests.java new file mode 100644 index 0000000000..59df61ff53 --- /dev/null +++ b/samples/boot/oauth2/authcodegrant/src/integration-test/java/org/springframework/security/samples/OAuth2AuthorizationCodeGrantApplicationTests.java @@ -0,0 +1,165 @@ +/* + * 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.samples; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; +import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Integration tests for the OAuth 2.0 client filters {@link OAuth2AuthorizationRequestRedirectFilter} + * and {@link OAuth2AuthorizationCodeGrantFilter}. These filters work together to realize + * the OAuth 2.0 Authorization Code Grant flow. + * + * @author Joe Grandja + * @since 5.1 + */ +@RunWith(SpringRunner.class) +@SpringBootTest +@AutoConfigureMockMvc +public class OAuth2AuthorizationCodeGrantApplicationTests { + @Autowired + private ClientRegistrationRepository clientRegistrationRepository; + + @Autowired + private OAuth2AuthorizedClientService authorizedClientService; + + @Autowired + private MockMvc mockMvc; + + @Test + public void requestWhenClientNotAuthorizedThenRedirectForAuthorization() throws Exception { + MvcResult mvcResult = this.mockMvc.perform(get("/repos").with(user("user"))) + .andExpect(status().is3xxRedirection()) + .andReturn(); + assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://github.com/login/oauth/authorize\\?response_type=code&client_id=your-app-client-id&scope=public_repo&state=.{15,}&redirect_uri=http://localhost/github-repos"); + } + + @Test + @DirtiesContext + public void requestWhenClientGrantedAuthorizationThenAuthorizedClientSaved() throws Exception { + // Setup the Authorization Request in the session + ClientRegistration registration = this.clientRegistrationRepository.findByRegistrationId("github"); + Map additionalParameters = new HashMap<>(); + additionalParameters.put(OAuth2ParameterNames.REGISTRATION_ID, registration.getRegistrationId()); + OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri(registration.getProviderDetails().getAuthorizationUri()) + .clientId(registration.getClientId()) + .redirectUri("http://localhost/github-repos") + .scopes(registration.getScopes()) + .state("state") + .additionalParameters(additionalParameters) + .build(); + + AuthorizationRequestRepository authorizationRequestRepository = + new HttpSessionOAuth2AuthorizationRequestRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response); + + MockHttpSession session = (MockHttpSession) request.getSession(); + + String principalName = "user"; + + // Authorization Response + this.mockMvc.perform(get("/github-repos") + .param(OAuth2ParameterNames.CODE, "code") + .param(OAuth2ParameterNames.STATE, "state") + .with(user(principalName)) + .session(session)) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("http://localhost/github-repos")); + + OAuth2AuthorizedClient authorizedClient = this.authorizedClientService.loadAuthorizedClient( + registration.getRegistrationId(), principalName); + assertThat(authorizedClient).isNotNull(); + } + + @EnableWebSecurity + static class OAuth2ClientConfig extends WebSecurityConfigurerAdapter { + // @formatter:off + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2() + .client() + .authorizationCodeGrant() + .tokenEndpoint() + .accessTokenResponseClient(this.accessTokenResponseClient()); + } + // @formatter:on + + private OAuth2AccessTokenResponseClient accessTokenResponseClient() { + OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("access-token-1234") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .expiresIn(60 * 1000) + .build(); + OAuth2AccessTokenResponseClient tokenResponseClient = mock(OAuth2AccessTokenResponseClient.class); + when(tokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse); + return tokenResponseClient; + } + } + + @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan(basePackages = "sample.web") + public static class SpringBootApplicationTestConfig { + } +} diff --git a/samples/boot/oauth2/authcodegrant/src/main/java/sample/OAuth2AuthorizationCodeGrantApplication.java b/samples/boot/oauth2/authcodegrant/src/main/java/sample/OAuth2AuthorizationCodeGrantApplication.java new file mode 100644 index 0000000000..d16dc5ad73 --- /dev/null +++ b/samples/boot/oauth2/authcodegrant/src/main/java/sample/OAuth2AuthorizationCodeGrantApplication.java @@ -0,0 +1,30 @@ +/* + * 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 sample; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Joe Grandja + */ +@SpringBootApplication +public class OAuth2AuthorizationCodeGrantApplication { + + public static void main(String[] args) { + SpringApplication.run(OAuth2AuthorizationCodeGrantApplication.class, args); + } +} diff --git a/samples/boot/oauth2/authcodegrant/src/main/java/sample/config/SecurityConfig.java b/samples/boot/oauth2/authcodegrant/src/main/java/sample/config/SecurityConfig.java new file mode 100644 index 0000000000..e755450b2d --- /dev/null +++ b/samples/boot/oauth2/authcodegrant/src/main/java/sample/config/SecurityConfig.java @@ -0,0 +1,55 @@ +/* + * 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 sample.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; + +/** + * @author Joe Grandja + */ +@EnableWebSecurity +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .formLogin() + .and() + .oauth2() + .client() + .authorizationCodeGrant(); + } + + @Bean + public UserDetailsService userDetailsService() { + UserDetails userDetails = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build(); + return new InMemoryUserDetailsManager(userDetails); + } +} diff --git a/samples/boot/oauth2/authcodegrant/src/main/java/sample/web/MainController.java b/samples/boot/oauth2/authcodegrant/src/main/java/sample/web/MainController.java new file mode 100644 index 0000000000..f0e525a270 --- /dev/null +++ b/samples/boot/oauth2/authcodegrant/src/main/java/sample/web/MainController.java @@ -0,0 +1,81 @@ +/* + * 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 sample.web; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.util.List; + +/** + * @author Joe Grandja + */ +@Controller +public class MainController { + @Autowired + private OAuth2AuthorizedClientService authorizedClientService; + + @GetMapping("/") + public String index() { + return "redirect:/repos"; + } + + @GetMapping("/repos") + public String gitHubRepos(Model model, Authentication authentication) { + String registrationId = "github"; + + OAuth2AuthorizedClient authorizedClient = + this.authorizedClientService.loadAuthorizedClient( + registrationId, authentication.getName()); + if (authorizedClient == null) { + throw new ClientAuthorizationRequiredException(registrationId); + } + + String endpointUri = "https://api.github.com/user/repos"; + List repos = WebClient.builder() + .filter(oauth2Credentials(authorizedClient)) + .build() + .get() + .uri(endpointUri) + .retrieve() + .bodyToMono(List.class) + .block(); + model.addAttribute("repos", repos); + + return "github-repos"; + } + + private ExchangeFilterFunction oauth2Credentials(OAuth2AuthorizedClient authorizedClient) { + return ExchangeFilterFunction.ofRequestProcessor( + clientRequest -> { + ClientRequest authorizedRequest = ClientRequest.from(clientRequest) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + authorizedClient.getAccessToken().getTokenValue()) + .build(); + return Mono.just(authorizedRequest); + }); + } +} diff --git a/samples/boot/oauth2/authcodegrant/src/main/resources/application.yml b/samples/boot/oauth2/authcodegrant/src/main/resources/application.yml new file mode 100644 index 0000000000..9ec59b1a1a --- /dev/null +++ b/samples/boot/oauth2/authcodegrant/src/main/resources/application.yml @@ -0,0 +1,23 @@ +server: + port: 8080 + +logging: + level: + root: INFO + org.springframework.web: INFO + org.springframework.security: INFO +# org.springframework.boot.autoconfigure: DEBUG + +spring: + thymeleaf: + cache: false + security: + oauth2: + client: + registration: + github: + client-id: your-app-client-id + client-secret: your-app-client-secret + scope: public_repo + redirect-uri-template: "{baseUrl}/github-repos" + client-name: GitHub Repositories diff --git a/samples/boot/oauth2/authcodegrant/src/main/resources/templates/github-repos.html b/samples/boot/oauth2/authcodegrant/src/main/resources/templates/github-repos.html new file mode 100644 index 0000000000..6cdd27bd89 --- /dev/null +++ b/samples/boot/oauth2/authcodegrant/src/main/resources/templates/github-repos.html @@ -0,0 +1,28 @@ + + + + Spring Security - OAuth 2.0 Authorization Code Grant + + + +

+
+ User: +
+
 
+
+
+ +
+
+
+

GitHub Repositories

+
+
    +
  • + : +
  • +
+
+ +