From 829c3867567b8be01a5b2e83d21efc5af8e744bc Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Mon, 20 Mar 2017 16:18:08 -0400 Subject: [PATCH] Add support for OAuth 2.0 Login Fixes gh-3907 --- config/pom.xml | 7 + config/spring-security-config.gradle | 1 + .../web/builders/FilterComparator.java | 8 + .../annotation/web/builders/HttpSecurity.java | 153 +++++++ ...ionCodeAuthenticationFilterConfigurer.java | 123 ++++++ ...onCodeRequestRedirectFilterConfigurer.java | 61 +++ .../oauth2/client/OAuth2LoginConfigurer.java | 154 +++++++ .../DefaultLoginPageConfigurerTests.groovy | 4 +- docs/manual/src/docs/asciidoc/index.adoc | 43 +- oauth2/oauth2-client/pom.xml | 156 +++++++ .../spring-security-oauth2-client.gradle | 12 + ...ionCodeAuthenticationProcessingFilter.java | 225 ++++++++++ ...thorizationCodeAuthenticationProvider.java | 120 ++++++ .../AuthorizationCodeAuthenticationToken.java | 63 +++ ...uthorizationCodeRequestRedirectFilter.java | 159 +++++++ ...AuthorizationGrantAuthenticationToken.java | 50 +++ .../AuthorizationGrantTokenExchanger.java | 40 ++ .../AuthorizationRequestRepository.java | 45 ++ .../AuthorizationRequestUriBuilder.java | 31 ++ ...DefaultAuthorizationRequestUriBuilder.java | 49 +++ .../authentication/DefaultStateGenerator.java | 54 +++ ...SessionAuthorizationRequestRepository.java | 63 +++ .../OAuth2AuthenticationException.java | 66 +++ .../OAuth2AuthenticationToken.java | 87 ++++ ...NimbusAuthorizationCodeTokenExchanger.java | 142 ++++++ .../registration/ClientRegistration.java | 279 ++++++++++++ .../ClientRegistrationProperties.java | 129 ++++++ .../ClientRegistrationRepository.java | 33 ++ .../InMemoryClientRegistrationRepository.java | 59 +++ .../oauth2/client/user/OAuth2UserService.java | 42 ++ .../AbstractOAuth2UserConverter.java | 53 +++ .../converter/CustomOAuth2UserConverter.java | 51 +++ .../user/converter/OAuth2UserConverter.java | 41 ++ .../user/converter/UserInfoConverter.java | 34 ++ .../user/nimbus/NimbusClientHttpResponse.java | 69 +++ .../user/nimbus/NimbusOAuth2UserService.java | 121 ++++++ ...horizationResponseAttributesConverter.java | 45 ++ .../ErrorResponseAttributesConverter.java | 53 +++ ...deAuthenticationProcessingFilterTests.java | 254 +++++++++++ ...izationCodeRequestRedirectFilterTests.java | 142 ++++++ .../client/authentication/TestUtil.java | 81 ++++ oauth2/oauth2-core/pom.xml | 138 ++++++ .../spring-security-oauth2-core.gradle | 9 + .../security/oauth2/core/AbstractToken.java | 59 +++ .../security/oauth2/core/AccessToken.java | 88 ++++ .../oauth2/core/AuthorizationGrantType.java | 46 ++ .../core/ClientAuthenticationMethod.java | 38 ++ .../security/oauth2/core/OAuth2Error.java | 74 ++++ ...onCodeAuthorizationResponseAttributes.java | 44 ++ ...thorizationCodeTokenRequestAttributes.java | 76 ++++ .../AuthorizationRequestAttributes.java | 127 ++++++ .../endpoint/ErrorResponseAttributes.java | 96 +++++ .../oauth2/core/endpoint/OAuth2Parameter.java | 46 ++ .../oauth2/core/endpoint/ResponseType.java | 46 ++ .../endpoint/TokenResponseAttributes.java | 109 +++++ .../oauth2/core/endpoint/package-info.java | 21 + .../security/oauth2/core/package-info.java | 19 + .../oauth2/core/user/DefaultOAuth2User.java | 153 +++++++ .../security/oauth2/core/user/OAuth2User.java | 55 +++ .../oauth2/core/user/package-info.java | 19 + .../oauth2/oidc/StandardClaimName.java | 69 +++ .../security/oauth2/oidc/package-info.java | 19 + .../oauth2/oidc/user/DefaultUserInfo.java | 154 +++++++ .../security/oauth2/oidc/user/UserInfo.java | 103 +++++ .../oauth2/oidc/user/package-info.java | 19 + samples/boot/helloworld/pom.xml | 7 + samples/boot/insecure/pom.xml | 7 + samples/boot/oauth2login/README.adoc | 342 +++++++++++++++ samples/boot/oauth2login/pom.xml | 173 ++++++++ ...g-security-samples-boot-oauth2login.gradle | 15 + .../samples/OAuth2LoginApplicationTests.java | 405 ++++++++++++++++++ .../ClientRegistrationAutoConfiguration.java | 129 ++++++ .../client/OAuth2LoginAutoConfiguration.java | 108 +++++ .../samples/OAuth2LoginApplication.java | 34 ++ .../samples/user/GitHubOAuth2User.java | 85 ++++ .../security/samples/web/MainController.java | 36 ++ .../META-INF/oauth2-clients-defaults.yml | 44 ++ .../main/resources/META-INF/spring.factories | 4 + .../src/main/resources/application.yml | 34 ++ .../src/main/resources/templates/index.html | 33 ++ .../ui/DefaultLoginPageGeneratingFilter.java | 50 ++- 81 files changed, 6485 insertions(+), 50 deletions(-) create mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/AuthorizationCodeAuthenticationFilterConfigurer.java create mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/AuthorizationCodeRequestRedirectFilterConfigurer.java create mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java create mode 100644 oauth2/oauth2-client/pom.xml create mode 100644 oauth2/oauth2-client/spring-security-oauth2-client.gradle create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProcessingFilter.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProvider.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationToken.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeRequestRedirectFilter.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationGrantAuthenticationToken.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationGrantTokenExchanger.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationRequestRepository.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationRequestUriBuilder.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/DefaultAuthorizationRequestUriBuilder.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/DefaultStateGenerator.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/HttpSessionAuthorizationRequestRepository.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationException.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationToken.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/nimbus/NimbusAuthorizationCodeTokenExchanger.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrationProperties.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrationRepository.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/InMemoryClientRegistrationRepository.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/OAuth2UserService.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/AbstractOAuth2UserConverter.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/CustomOAuth2UserConverter.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/OAuth2UserConverter.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/UserInfoConverter.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/nimbus/NimbusClientHttpResponse.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/nimbus/NimbusOAuth2UserService.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/converter/AuthorizationCodeAuthorizationResponseAttributesConverter.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/converter/ErrorResponseAttributesConverter.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProcessingFilterTests.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeRequestRedirectFilterTests.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/TestUtil.java create mode 100644 oauth2/oauth2-core/pom.xml create mode 100644 oauth2/oauth2-core/spring-security-oauth2-core.gradle create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AbstractToken.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AccessToken.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClientAuthenticationMethod.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2Error.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/AuthorizationCodeAuthorizationResponseAttributes.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/AuthorizationCodeTokenRequestAttributes.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/AuthorizationRequestAttributes.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/ErrorResponseAttributes.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2Parameter.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/ResponseType.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/TokenResponseAttributes.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/package-info.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/package-info.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/DefaultOAuth2User.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/OAuth2User.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/package-info.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/StandardClaimName.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/package-info.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/DefaultUserInfo.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/UserInfo.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/package-info.java create mode 100644 samples/boot/oauth2login/README.adoc create mode 100644 samples/boot/oauth2login/pom.xml create mode 100644 samples/boot/oauth2login/spring-security-samples-boot-oauth2login.gradle create mode 100644 samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java create mode 100644 samples/boot/oauth2login/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ClientRegistrationAutoConfiguration.java create mode 100644 samples/boot/oauth2login/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2LoginAutoConfiguration.java create mode 100644 samples/boot/oauth2login/src/main/java/org/springframework/security/samples/OAuth2LoginApplication.java create mode 100644 samples/boot/oauth2login/src/main/java/org/springframework/security/samples/user/GitHubOAuth2User.java create mode 100644 samples/boot/oauth2login/src/main/java/org/springframework/security/samples/web/MainController.java create mode 100644 samples/boot/oauth2login/src/main/resources/META-INF/oauth2-clients-defaults.yml create mode 100644 samples/boot/oauth2login/src/main/resources/META-INF/spring.factories create mode 100644 samples/boot/oauth2login/src/main/resources/application.yml create mode 100644 samples/boot/oauth2login/src/main/resources/templates/index.html diff --git a/config/pom.xml b/config/pom.xml index 1aaa31bc28..40e304b097 100644 --- a/config/pom.xml +++ b/config/pom.xml @@ -113,6 +113,13 @@ compile true + + org.springframework.security + spring-security-oauth2-client + 5.0.0.BUILD-SNAPSHOT + compile + true + org.springframework.security spring-security-openid diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index 559a9d3f8f..4e95a7ee37 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -12,6 +12,7 @@ dependencies { optional project(':spring-security-ldap') optional project(':spring-security-messaging') + optional project(':spring-security-oauth2-client') optional project(':spring-security-openid') optional project(':spring-security-web') optional 'org.aspectj:aspectjweaver' 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 d5907fa1d4..abd4086272 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 @@ -77,6 +77,10 @@ final class FilterComparator implements Comparator, Serializable { order += STEP; put(LogoutFilter.class, order); order += STEP; + filterToOrder.put( + "org.springframework.security.oauth2.client.authentication.AuthorizationCodeRequestRedirectFilter", + order); + order += STEP; put(X509AuthenticationFilter.class, order); order += STEP; put(AbstractPreAuthenticatedProcessingFilter.class, order); @@ -84,6 +88,10 @@ final class FilterComparator implements Comparator, Serializable { filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter", order); order += STEP; + filterToOrder.put( + "org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationProcessingFilter", + order); + order += STEP; put(UsernamePasswordAuthenticationFilter.class, order); order += STEP; put(ConcurrentSessionFilter.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 0c3e2ad437..b792351d88 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 @@ -61,6 +61,7 @@ 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; @@ -896,6 +897,158 @@ public final class HttpSecurity extends return getOrApply(new FormLoginConfigurer()); } + /** + * Configures authentication against an external OAuth 2.0 or OpenID Connect 1.0 Provider. + *
+ *
+ * + * The "authentication flow" is realized using the Authorization Code Grant, + * as specified in the OAuth 2.0 Authorization Framework. + *
+ *
+ * + * As a prerequisite to using this feature, the developer must register a Client with an Authorization Server. + * The output of the Client Registration process results in a number of properties that are then used for configuring + * an instance of a {@link org.springframework.security.oauth2.client.registration.ClientRegistration}. + * Properties specific to a Client include: client_id, client_secret, scope, redirect_uri, etc. + * There are also properties specific to the Provider, for example, + * Authorization Endpoint URI, Token Endpoint URI, UserInfo Endpoint URI, etc. + *
+ *
+ * + * Multiple client support is provided for use cases where the application provides the user the option + * for "Logging in" against one or more Providers, for example, Google, GitHub, Facebook, etc. + *
+ *
+ * + * {@link org.springframework.security.oauth2.client.registration.ClientRegistration}(s) are composed within a + * {@link org.springframework.security.oauth2.client.registration.ClientRegistrationRepository}. + * An instance of {@link org.springframework.security.oauth2.client.registration.ClientRegistrationRepository} is required + * and may be supplied via the {@link ApplicationContext} or configured using + * {@link OAuth2LoginConfigurer#clients(org.springframework.security.oauth2.client.registration.ClientRegistrationRepository)}. + *
+ *
+ * + * The default configuration provides an auto-generated login page at "/login" and + * redirects to "/login?error" when an authentication error occurs. + * The login page will display each of the clients (composed within the + * {@link org.springframework.security.oauth2.client.registration.ClientRegistrationRepository}) + * with an anchor link to "/oauth2/authorization/code/{clientAlias}". + * Clicking through the link will initiate the "Authorization Request" flow + * redirecting the end-user's user-agent to the Authorization Endpoint of the Provider. + * Assuming the Resource Owner (end-user) grants the Client access, the Authorization Server + * will redirect the end-user's user-agent to the Redirection Endpoint containing the Authorization Code + * - the Redirection Endpoint is automatically configured for the application and + * defaults to "/oauth2/authorize/code/{clientAlias}". + * + *

+ * At this point in the "authentication flow", the configured + * {@link org.springframework.security.oauth2.client.authentication.AuthorizationGrantTokenExchanger} + * will exchange the Authorization Code for an Access Token and then use it to access the protected resource + * at the UserInfo Endpoint (via {@link org.springframework.security.oauth2.client.user.OAuth2UserService}) + * in order to retrieve the details of the Resource Owner (end-user) and establish the "authenticated" session. + * + *

Example Configurations

+ * + * The minimal configuration defaults to automatically generating a login page at "/login" + * and redirecting to "/login?error" when an authentication error occurs or redirecting to + * "/" when an authenticated session is established. + * + *
+	 * @EnableWebSecurity
+	 * public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 *		http
+	 * 			.authorizeRequests()
+	 * 				.anyRequest().authenticated()
+	 * 				.and()
+	 * 			.oauth2Login();
+	 * 	}
+	 *
+	 *	@Bean
+	 *	public ClientRegistrationRepository clientRegistrationRepository() {
+	 *		// ClientRegistrationRepositoryImpl must be composed of at least one ClientRegistration instance
+	 *		return new ClientRegistrationRepositoryImpl();
+	 *	}
+	 * }
+	 * 
+ * + * The following shows the configuration options available for customizing the defaults. + * + *
+	 * @EnableWebSecurity
+	 * public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 *		http
+	 * 			.authorizeRequests()
+	 * 				.anyRequest().authenticated()
+	 * 				.and()
+	 * 			.oauth2Login()
+	 * 				.clients(this.clientRegistrationRepository())
+	 * 				.authorizationRequestBuilder(this.authorizationRequestBuilder())
+	 * 				.authorizationCodeTokenExchanger(this.authorizationCodeTokenExchanger())
+	 * 				.userInfoEndpoint()
+	 * 					.userInfoService(this.userInfoService())
+	 * 				.userInfoEndpoint()
+	 * 					// Provide a mapping between a Converter implementation and a UserInfo Endpoint URI
+	 * 					.userInfoTypeConverter(this.userInfoConverter(),
+	 * 									new URI("https://www.googleapis.com/oauth2/v3/userinfo"));
+	 * 	}
+	 *
+	 *	@Bean
+	 *	public ClientRegistrationRepository clientRegistrationRepository() {
+	 *		// ClientRegistrationRepositoryImpl must be composed of at least one ClientRegistration instance
+	 *		return new ClientRegistrationRepositoryImpl();
+	 *	}
+	 *
+	 * 	@Bean
+	 * 	public AuthorizationRequestUriBuilder authorizationRequestBuilder() {
+	 * 		// Custom URI builder for the "Authorization Request"
+	 * 		return new AuthorizationRequestUriBuilderImpl();
+	 * 	}
+	 *
+	 * 	@Bean
+	 * 	public AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> authorizationCodeTokenExchanger() {
+	 * 		// Custom implementation that exchanges an "Authorization Code Grant" for an "Access Token"
+	 * 		return new AuthorizationCodeTokenExchangerImpl();
+	 * 	}
+	 *
+	 * 	@Bean
+	 * 	public OAuth2UserService userInfoService() {
+	 * 		// Custom implementation that retrieves the details of the authenticated user at the "UserInfo Endpoint"
+	 * 		return new OAuth2UserServiceImpl();
+	 * 	}
+	 *
+	 * 	@Bean
+	 * 	public Converter<ClientHttpResponse, UserInfo> userInfoConverter() {
+	 * 		// Default converter implementation for UserInfo
+	 * 		return new org.springframework.security.oauth2.client.user.converter.UserInfoConverter();
+	 * 	}
+	 * }
+	 * 
+ * + * @author Joe Grandja + * @since 5.0 + * @see Section 4.1 Authorization Code Grant Flow + * @see Section 4.1.1 Authorization Request + * @see Section 4.1.2 Authorization Response + * @see org.springframework.security.oauth2.client.registration.ClientRegistration + * @see org.springframework.security.oauth2.client.registration.ClientRegistrationRepository + * @see org.springframework.security.oauth2.client.authentication.AuthorizationRequestUriBuilder + * @see org.springframework.security.oauth2.client.authentication.AuthorizationGrantTokenExchanger + * @see org.springframework.security.oauth2.client.user.OAuth2UserService + * + * @return the {@link OAuth2LoginConfigurer} for further customizations + * @throws Exception + */ + public OAuth2LoginConfigurer oauth2Login() throws Exception { + return getOrApply(new OAuth2LoginConfigurer()); + } + /** * 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/client/AuthorizationCodeAuthenticationFilterConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/AuthorizationCodeAuthenticationFilterConfigurer.java new file mode 100644 index 0000000000..508330b748 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/AuthorizationCodeAuthenticationFilterConfigurer.java @@ -0,0 +1,123 @@ +/* + * Copyright 2012-2017 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.core.convert.converter.Converter; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer; +import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationProcessingFilter; +import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationProvider; +import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationToken; +import org.springframework.security.oauth2.client.authentication.AuthorizationGrantTokenExchanger; +import org.springframework.security.oauth2.client.authentication.nimbus.NimbusAuthorizationCodeTokenExchanger; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.user.OAuth2UserService; +import org.springframework.security.oauth2.client.user.nimbus.NimbusOAuth2UserService; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.Assert; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; + +/** + * @author Joe Grandja + */ +final class AuthorizationCodeAuthenticationFilterConfigurer> extends + AbstractAuthenticationFilterConfigurer, AuthorizationCodeAuthenticationProcessingFilter> { + + private AuthorizationGrantTokenExchanger authorizationCodeTokenExchanger; + private OAuth2UserService userInfoService; + private Map> userInfoTypeConverters = new HashMap<>(); + + + AuthorizationCodeAuthenticationFilterConfigurer() { + super(new AuthorizationCodeAuthenticationProcessingFilter(), null); + } + + AuthorizationCodeAuthenticationFilterConfigurer clientRegistrationRepository(ClientRegistrationRepository clientRegistrationRepository) { + Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); + Assert.notEmpty(clientRegistrationRepository.getRegistrations(), "clientRegistrationRepository cannot be empty"); + this.getBuilder().setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository); + return this; + } + + AuthorizationCodeAuthenticationFilterConfigurer authorizationCodeTokenExchanger( + AuthorizationGrantTokenExchanger authorizationCodeTokenExchanger) { + + Assert.notNull(authorizationCodeTokenExchanger, "authorizationCodeTokenExchanger cannot be null"); + this.authorizationCodeTokenExchanger = authorizationCodeTokenExchanger; + return this; + } + + AuthorizationCodeAuthenticationFilterConfigurer userInfoService(OAuth2UserService userInfoService) { + Assert.notNull(userInfoService, "userInfoService cannot be null"); + this.userInfoService = userInfoService; + return this; + } + + AuthorizationCodeAuthenticationFilterConfigurer userInfoTypeConverter(Converter userInfoConverter, URI userInfoUri) { + Assert.notNull(userInfoConverter, "userInfoConverter cannot be null"); + Assert.notNull(userInfoUri, "userInfoUri cannot be null"); + this.userInfoTypeConverters.put(userInfoUri, userInfoConverter); + return this; + } + + String getLoginUrl() { + return super.getLoginPage(); + } + + String getLoginFailureUrl() { + return super.getFailureUrl(); + } + + @Override + public void init(H http) throws Exception { + AuthorizationCodeAuthenticationProvider authenticationProvider = new AuthorizationCodeAuthenticationProvider( + this.getAuthorizationCodeTokenExchanger(), this.getUserInfoService()); + authenticationProvider = this.postProcess(authenticationProvider); + http.authenticationProvider(authenticationProvider); + super.init(http); + } + + @Override + public void configure(H http) throws Exception { + AuthorizationCodeAuthenticationProcessingFilter authFilter = this.getAuthenticationFilter(); + authFilter.setClientRegistrationRepository(OAuth2LoginConfigurer.getClientRegistrationRepository(this.getBuilder())); + super.configure(http); + } + + @Override + protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) { + return this.getAuthenticationFilter().getAuthorizeRequestMatcher(); + } + + private AuthorizationGrantTokenExchanger getAuthorizationCodeTokenExchanger() { + if (this.authorizationCodeTokenExchanger == null) { + this.authorizationCodeTokenExchanger = new NimbusAuthorizationCodeTokenExchanger(); + } + return this.authorizationCodeTokenExchanger; + } + + private OAuth2UserService getUserInfoService() { + if (this.userInfoService == null) { + this.userInfoService = new NimbusOAuth2UserService(this.userInfoTypeConverters); + } + return this.userInfoService; + } +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/AuthorizationCodeRequestRedirectFilterConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/AuthorizationCodeRequestRedirectFilterConfigurer.java new file mode 100644 index 0000000000..ec506f0de1 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/AuthorizationCodeRequestRedirectFilterConfigurer.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2017 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.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.oauth2.client.authentication.AuthorizationCodeRequestRedirectFilter; +import org.springframework.security.oauth2.client.authentication.AuthorizationRequestUriBuilder; +import org.springframework.security.oauth2.client.authentication.DefaultAuthorizationRequestUriBuilder; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.util.Assert; + +/** + * @author Joe Grandja + */ +final class AuthorizationCodeRequestRedirectFilterConfigurer> extends + AbstractHttpConfigurer, B> { + + private AuthorizationRequestUriBuilder authorizationRequestBuilder; + + AuthorizationCodeRequestRedirectFilterConfigurer clientRegistrationRepository(ClientRegistrationRepository clientRegistrationRepository) { + Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); + Assert.notEmpty(clientRegistrationRepository.getRegistrations(), "clientRegistrationRepository cannot be empty"); + this.getBuilder().setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository); + return this; + } + + AuthorizationCodeRequestRedirectFilterConfigurer authorizationRequestBuilder(AuthorizationRequestUriBuilder authorizationRequestBuilder) { + Assert.notNull(authorizationRequestBuilder, "authorizationRequestBuilder cannot be null"); + this.authorizationRequestBuilder = authorizationRequestBuilder; + return this; + } + + @Override + public void configure(B http) throws Exception { + AuthorizationCodeRequestRedirectFilter filter = new AuthorizationCodeRequestRedirectFilter( + OAuth2LoginConfigurer.getClientRegistrationRepository(this.getBuilder()), + this.getAuthorizationRequestBuilder()); + http.addFilter(this.postProcess(filter)); + } + + private AuthorizationRequestUriBuilder getAuthorizationRequestBuilder() { + if (this.authorizationRequestBuilder == null) { + this.authorizationRequestBuilder = new DefaultAuthorizationRequestUriBuilder(); + } + return this.authorizationRequestBuilder; + } +} 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 new file mode 100644 index 0000000000..c619d1be9d --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java @@ -0,0 +1,154 @@ +/* + * Copyright 2012-2017 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.context.ApplicationContext; +import org.springframework.core.convert.converter.Converter; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationToken; +import org.springframework.security.oauth2.client.authentication.AuthorizationCodeRequestRedirectFilter; +import org.springframework.security.oauth2.client.authentication.AuthorizationGrantTokenExchanger; +import org.springframework.security.oauth2.client.authentication.AuthorizationRequestUriBuilder; +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.user.OAuth2UserService; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +import java.net.URI; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author Joe Grandja + */ +public final class OAuth2LoginConfigurer> extends + AbstractHttpConfigurer, B> { + + private final AuthorizationCodeRequestRedirectFilterConfigurer authorizationCodeRequestRedirectFilterConfigurer; + private final AuthorizationCodeAuthenticationFilterConfigurer authorizationCodeAuthenticationFilterConfigurer; + private final UserInfoEndpointConfig userInfoEndpointConfig; + + public OAuth2LoginConfigurer() { + this.authorizationCodeRequestRedirectFilterConfigurer = new AuthorizationCodeRequestRedirectFilterConfigurer<>(); + this.authorizationCodeAuthenticationFilterConfigurer = new AuthorizationCodeAuthenticationFilterConfigurer<>(); + this.userInfoEndpointConfig = new UserInfoEndpointConfig(); + } + + public OAuth2LoginConfigurer clients(ClientRegistration... clientRegistrations) { + Assert.notEmpty(clientRegistrations, "clientRegistrations cannot be empty"); + return clients(new InMemoryClientRegistrationRepository(Arrays.asList(clientRegistrations))); + } + + public OAuth2LoginConfigurer clients(ClientRegistrationRepository clientRegistrationRepository) { + Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); + Assert.notEmpty(clientRegistrationRepository.getRegistrations(), "clientRegistrationRepository cannot be empty"); + this.getBuilder().setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository); + return this; + } + + public OAuth2LoginConfigurer authorizationRequestBuilder(AuthorizationRequestUriBuilder authorizationRequestBuilder) { + Assert.notNull(authorizationRequestBuilder, "authorizationRequestBuilder cannot be null"); + this.authorizationCodeRequestRedirectFilterConfigurer.authorizationRequestBuilder(authorizationRequestBuilder); + return this; + } + + public OAuth2LoginConfigurer authorizationCodeTokenExchanger( + AuthorizationGrantTokenExchanger authorizationCodeTokenExchanger) { + + Assert.notNull(authorizationCodeTokenExchanger, "authorizationCodeTokenExchanger cannot be null"); + this.authorizationCodeAuthenticationFilterConfigurer.authorizationCodeTokenExchanger(authorizationCodeTokenExchanger); + return this; + } + + public UserInfoEndpointConfig userInfoEndpoint() { + return this.userInfoEndpointConfig; + } + + public class UserInfoEndpointConfig { + + private UserInfoEndpointConfig() { + } + + public OAuth2LoginConfigurer userInfoService(OAuth2UserService userInfoService) { + Assert.notNull(userInfoService, "userInfoService cannot be null"); + OAuth2LoginConfigurer.this.authorizationCodeAuthenticationFilterConfigurer.userInfoService(userInfoService); + return this.and(); + } + + public OAuth2LoginConfigurer userInfoTypeConverter(Converter userInfoConverter, URI userInfoUri) { + Assert.notNull(userInfoConverter, "userInfoConverter cannot be null"); + Assert.notNull(userInfoUri, "userInfoUri cannot be null"); + OAuth2LoginConfigurer.this.authorizationCodeAuthenticationFilterConfigurer.userInfoTypeConverter(userInfoConverter, userInfoUri); + return this.and(); + } + + public OAuth2LoginConfigurer and() { + return OAuth2LoginConfigurer.this; + } + } + + @Override + public void init(B http) throws Exception { + this.authorizationCodeRequestRedirectFilterConfigurer.setBuilder(http); + this.authorizationCodeAuthenticationFilterConfigurer.setBuilder(http); + + this.authorizationCodeRequestRedirectFilterConfigurer.init(http); + this.authorizationCodeAuthenticationFilterConfigurer.init(http); + this.initDefaultLoginFilter(http); + } + + @Override + public void configure(B http) throws Exception { + this.authorizationCodeRequestRedirectFilterConfigurer.configure(http); + this.authorizationCodeAuthenticationFilterConfigurer.configure(http); + } + + static > ClientRegistrationRepository getClientRegistrationRepository(B http) { + ClientRegistrationRepository clientRegistrationRepository = http.getSharedObject(ClientRegistrationRepository.class); + if (clientRegistrationRepository == null) { + clientRegistrationRepository = getDefaultClientRegistrationRepository(http); + http.setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository); + } + return clientRegistrationRepository; + } + + private static > ClientRegistrationRepository getDefaultClientRegistrationRepository(B http) { + return http.getSharedObject(ApplicationContext.class).getBean(ClientRegistrationRepository.class); + } + + private void initDefaultLoginFilter(B http) { + DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http.getSharedObject(DefaultLoginPageGeneratingFilter.class); + if (loginPageGeneratingFilter != null && !this.authorizationCodeAuthenticationFilterConfigurer.isCustomLoginPage()) { + ClientRegistrationRepository clientRegistrationRepository = getClientRegistrationRepository(this.getBuilder()); + if (!CollectionUtils.isEmpty(clientRegistrationRepository.getRegistrations())) { + Map oauth2AuthenticationUrlToClientName = clientRegistrationRepository.getRegistrations().stream() + .collect(Collectors.toMap(e -> AuthorizationCodeRequestRedirectFilter.AUTHORIZATION_BASE_URI + "/" + e.getClientAlias(), + e -> e.getClientName())); + loginPageGeneratingFilter.setOauth2LoginEnabled(true); + loginPageGeneratingFilter.setOauth2AuthenticationUrlToClientName(oauth2AuthenticationUrlToClientName); + loginPageGeneratingFilter.setLoginPageUrl(this.authorizationCodeAuthenticationFilterConfigurer.getLoginUrl()); + loginPageGeneratingFilter.setFailureUrl(this.authorizationCodeAuthenticationFilterConfigurer.getLoginFailureUrl()); + } + } + } +} diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.groovy index ac128b1fe9..25f10dbba7 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.groovy @@ -78,7 +78,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { springSecurityFilterChain.doFilter(request,response,chain) then: response.getContentAsString() == """Login Page -

Your login attempt was not successful, try again.

Reason: Bad credentials

Login with Username and Password

+

Your login attempt was not successful, try again.

Reason: Bad credentials

Login with Username and Password

@@ -107,7 +107,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec { springSecurityFilterChain.doFilter(request,response,chain) then: "sent to default success page" response.getContentAsString() == """Login Page -

You have been logged out

Login with Username and Password

+

You have been logged out

Login with Username and Password

User:
Password:
diff --git a/docs/manual/src/docs/asciidoc/index.adoc b/docs/manual/src/docs/asciidoc/index.adoc index 12baa641d4..3e78637639 100644 --- a/docs/manual/src/docs/asciidoc/index.adoc +++ b/docs/manual/src/docs/asciidoc/index.adoc @@ -375,42 +375,15 @@ git clone https://github.com/spring-projects/spring-security.git This will give you access to the entire project history (including all releases and branches) on your local machine. [[new]] -== What's New in Spring Security 4.2 - -Among other things, Spring Security 4.2 brings early support for Spring Framework 5. -You can find the change logs for https://github.com/spring-projects/spring-security/milestone/86?closed=1[4.2.0.M1], https://github.com/spring-projects/spring-security/milestone/91?closed=1[4.2.0.RC1], https://github.com/spring-projects/spring-security/milestone/92?closed=1[4.2.0.RELEASE] which closes over 80 issues. -The overwhelming majority of these features were contributed by the community. -Below you can find the highlights of this release. - -=== Web Improvements - -* https://github.com/spring-projects/spring-security/pull/3812[#3812] - <> -* https://github.com/spring-projects/spring-security/pull/4116[#4116] - <> -* https://github.com/spring-projects/spring-security/pull/3938[#3938] - Add <> -* https://github.com/spring-projects/spring-security/issues/3949[#3949] - Add <>. -* https://github.com/spring-projects/spring-security/pull/3978[#3978] - Support for Standford WebAuth and Shibboleth using the newly added http://docs.spring.io/spring-security/site/docs/4.2.x-SNAPSHOT/apidocs/org/springframework/security/web/authentication/preauth/RequestAttributeAuthenticationFilter.html[RequestAttributeAuthenticationFilter]. -* https://github.com/spring-projects/spring-security/issues/4076[#4076] - Document <> Configuration -* https://github.com/spring-projects/spring-security/issues/3795[#3795] - `ConcurrentSessionFilter` supports `InvalidSessionStrategy` -* https://github.com/spring-projects/spring-security/pull/3904[#3904] - Add `CompositeLogoutHandler` - -=== Configuration Improvements - -* https://github.com/spring-projects/spring-security/pull/3956[#3956] - Central configuration of the http://docs.spring.io/spring-security/site/migrate/current/3-to-4/html5/migrate-3-to-4-jc.html#m3to4-role-prefixing[default role prefix]. See the issue for details. -* https://github.com/spring-projects/spring-security/issues/4102[#4102] - Custom default configuration in `WebSecurityConfigurerAdapter`. See <> -* https://github.com/spring-projects/spring-security/issues/3899[#3899] - <> supports unlimited sessions. -* https://github.com/spring-projects/spring-security/issues/4097[#4097] - <> adds more powerful request matching support to the XML namespace. -* https://github.com/spring-projects/spring-security/issues/3990[#3990] - Support for constructing `RoleHierarchy` from `Map` (i.e. `yml`) -* https://github.com/spring-projects/spring-security/pull/4062[#4062] - Custom cookiePath to `CookieCsrfTokenRepository` -* https://github.com/spring-projects/spring-security/issues/3794[#3794] - Allow configuration of `InvalidSessionStrategy` on `SessionManagementConfigurer` -* https://github.com/spring-projects/spring-security/issues/4020[#4020] - Fix Exposing Beans for defaultMethodExpressionHandler can prevent Method Security - -=== Miscellaneous - -* https://github.com/spring-projects/spring-security/issues/4080[#4080] - Spring 5 support -* https://github.com/spring-projects/spring-security/issues/4095[#4095] - `Add UserBuilder` -* https://github.com/spring-projects/spring-security/issues/4018[#4018] - Fix after `csrf()` is invoked, future `MockMvc` invocations use original `CsrfTokenRepository` -* Version Updates +== What's New in Spring Security 5.0 +Spring Security 5.0 provides a number of new features as well as support for Spring Framework 5. +You can find the change log at https://github.com/spring-projects/spring-security/milestone/90?closed=1[5.0.0.M1]. +Below are the highlights of this milestone release. + +=== New Features + +* https://github.com/spring-projects/spring-security/issues/3907[#3907] - Support added for OAuth 2.0 Login (start with {gh-samples-url}/boot/oauth2login/README.adoc[Sample README]) [[samples]] == Samples and Guides (Start Here) diff --git a/oauth2/oauth2-client/pom.xml b/oauth2/oauth2-client/pom.xml new file mode 100644 index 0000000000..63a98052b3 --- /dev/null +++ b/oauth2/oauth2-client/pom.xml @@ -0,0 +1,156 @@ + + + 4.0.0 + org.springframework.security + spring-security-oauth2-client + 5.0.0.BUILD-SNAPSHOT + spring-security-oauth2-client + spring-security-oauth2-client + http://spring.io/spring-security + + spring.io + http://spring.io/ + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + rwinch + Rob Winch + rwinch@pivotal.io + + + jgrandja + Joe Grandja + jgrandja@pivotal.io + + + + scm:git:git://github.com/spring-projects/spring-security + scm:git:git://github.com/spring-projects/spring-security + https://github.com/spring-projects/spring-security + + + + + org.springframework + spring-framework-bom + 4.3.5.RELEASE + pom + import + + + + + + com.nimbusds + oauth2-oidc-sdk + 5.21 + compile + + + org.springframework.security + spring-security-core + 5.0.0.BUILD-SNAPSHOT + compile + + + org.springframework.security + spring-security-oauth2-core + 5.0.0.BUILD-SNAPSHOT + compile + + + org.springframework.security + spring-security-web + 5.0.0.BUILD-SNAPSHOT + compile + + + org.springframework + spring-core + compile + + + commons-logging + commons-logging + + + + + org.springframework + spring-web + compile + + + commons-logging + commons-logging + 1.2 + compile + true + + + javax.servlet + javax.servlet-api + 3.1.0 + provided + + + ch.qos.logback + logback-classic + 1.1.2 + test + + + junit + junit + 4.12 + test + + + org.assertj + assertj-core + 3.6.2 + test + + + org.mockito + mockito-core + 1.10.19 + test + + + org.slf4j + jcl-over-slf4j + 1.7.7 + test + + + org.springframework + spring-test + test + + + + + spring-snapshot + https://repo.spring.io/snapshot + + + + + + maven-compiler-plugin + + 1.8 + 1.8 + + + + + diff --git a/oauth2/oauth2-client/spring-security-oauth2-client.gradle b/oauth2/oauth2-client/spring-security-oauth2-client.gradle new file mode 100644 index 0000000000..b0834f886e --- /dev/null +++ b/oauth2/oauth2-client/spring-security-oauth2-client.gradle @@ -0,0 +1,12 @@ +apply plugin: 'io.spring.convention.spring-module' + +dependencies { + compile project(':spring-security-core') + compile project(':spring-security-oauth2-core') + compile project(':spring-security-web') + compile springCoreDependency + compile 'com.nimbusds:oauth2-oidc-sdk' + compile 'org.springframework:spring-web' + + provided 'javax.servlet:javax.servlet-api' +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProcessingFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProcessingFilter.java new file mode 100644 index 0000000000..bc8c144123 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProcessingFilter.java @@ -0,0 +1,225 @@ +/* + * Copyright 2012-2017 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.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.user.OAuth2UserService; +import org.springframework.security.oauth2.client.web.converter.AuthorizationCodeAuthorizationResponseAttributesConverter; +import org.springframework.security.oauth2.client.web.converter.ErrorResponseAttributesConverter; +import org.springframework.security.oauth2.core.AccessToken; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.endpoint.*; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static org.springframework.security.oauth2.client.authentication.AuthorizationCodeRequestRedirectFilter.isDefaultRedirectUri; + +/** + * An implementation of an {@link AbstractAuthenticationProcessingFilter} that handles + * the processing of an OAuth 2.0 Authorization Response for the authorization code grant flow. + * + *

+ * This Filter processes the Authorization Response in the following step sequence: + * + *

    + *
  1. + * Assuming the resource owner (end-user) has granted access to the client, the authorization server will append the + * {@link OAuth2Parameter#CODE} and {@link OAuth2Parameter#STATE} (if provided in the Authorization Request) parameters + * to the {@link OAuth2Parameter#REDIRECT_URI} (provided in the Authorization Request) + * and redirect the end-user's user-agent back to this Filter (the client). + *
  2. + *
  3. + * This Filter will then create an {@link AuthorizationCodeAuthenticationToken} with + * the {@link OAuth2Parameter#CODE} received in the previous step and pass it to + * {@link AuthorizationCodeAuthenticationProvider#authenticate(Authentication)} (indirectly via {@link AuthenticationManager}). + * The {@link AuthorizationCodeAuthenticationProvider} will use an {@link AuthorizationGrantTokenExchanger} to make a request + * to the authorization server's Token Endpoint for exchanging the {@link OAuth2Parameter#CODE} for an {@link AccessToken}. + *
  4. + *
  5. + * Upon receiving the Access Token Request, the authorization server will authenticate the client, + * verify the {@link OAuth2Parameter#CODE}, and ensure that the {@link OAuth2Parameter#REDIRECT_URI} + * received matches the URI originally provided in the Authorization Request. + * If the request is valid, the authorization server will respond back with a {@link TokenResponseAttributes}. + *
  6. + *
  7. + * The {@link AuthorizationCodeAuthenticationProvider} will then create a new {@link OAuth2AuthenticationToken} + * associating the {@link AccessToken} from the {@link TokenResponseAttributes} and pass it to + * {@link OAuth2UserService#loadUser(OAuth2AuthenticationToken)}. The {@link OAuth2UserService} will make a request + * to the authorization server's UserInfo Endpoint (using the {@link AccessToken}) + * to obtain the end-user's (resource owner) attributes and return it in the form of an {@link OAuth2User}. + *
  8. + *
  9. + * The {@link AuthorizationCodeAuthenticationProvider} will create another new {@link OAuth2AuthenticationToken} + * but this time associating the {@link AccessToken} and {@link OAuth2User} returned from the {@link OAuth2UserService}. + * Finally, the {@link OAuth2AuthenticationToken} is returned to the {@link AuthenticationManager} + * and then back to this Filter at which point the session is considered "authenticated". + *
  10. + *
+ * + *

+ * NOTE: Steps 4-5 are not part of the authorization code grant flow and instead are + * "authentication flow" steps that are required in order to authenticate the end-user with the system. + * + * @author Joe Grandja + * @since 5.0 + * @see AbstractAuthenticationProcessingFilter + * @see AuthorizationCodeAuthenticationToken + * @see AuthorizationCodeAuthenticationProvider + * @see AuthorizationGrantTokenExchanger + * @see AuthorizationCodeAuthorizationResponseAttributes + * @see AuthorizationRequestAttributes + * @see AuthorizationRequestRepository + * @see AuthorizationCodeRequestRedirectFilter + * @see ClientRegistration + * @see ClientRegistrationRepository + * @see Section 4.1 Authorization Code Grant Flow + * @see Section 4.1.2 Authorization Response + */ +public class AuthorizationCodeAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter { + public static final String AUTHORIZE_BASE_URI = "/oauth2/authorize/code"; + private static final String CLIENT_ALIAS_VARIABLE_NAME = "clientAlias"; + private static final String AUTHORIZE_URI = AUTHORIZE_BASE_URI + "/{" + CLIENT_ALIAS_VARIABLE_NAME + "}"; + private static final String AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE = "authorization_request_not_found"; + 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 ErrorResponseAttributesConverter errorResponseConverter = new ErrorResponseAttributesConverter(); + private final AuthorizationCodeAuthorizationResponseAttributesConverter authorizationCodeResponseConverter = + new AuthorizationCodeAuthorizationResponseAttributesConverter(); + private final RequestMatcher authorizeRequestMatcher = new AntPathRequestMatcher(AUTHORIZE_URI); + private ClientRegistrationRepository clientRegistrationRepository; + private AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionAuthorizationRequestRepository(); + + public AuthorizationCodeAuthenticationProcessingFilter() { + super(AUTHORIZE_URI); + } + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) + throws AuthenticationException, IOException, ServletException { + + ErrorResponseAttributes authorizationError = this.errorResponseConverter.convert(request); + if (authorizationError != null) { + OAuth2Error oauth2Error = new OAuth2Error(authorizationError.getErrorCode(), + authorizationError.getDescription(), authorizationError.getUri()); + this.getAuthorizationRequestRepository().removeAuthorizationRequest(request); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + } + + AuthorizationRequestAttributes matchingAuthorizationRequest = this.resolveAuthorizationRequest(request); + + ClientRegistration clientRegistration = this.getClientRegistrationRepository().getRegistrationByClientId( + matchingAuthorizationRequest.getClientId()); + + // If clientRegistration.redirectUri is the default one (with Uri template variables) + // then use matchingAuthorizationRequest.redirectUri instead + if (isDefaultRedirectUri(clientRegistration)) { + clientRegistration = new ClientRegistrationBuilderWithUriOverrides( + clientRegistration, matchingAuthorizationRequest.getRedirectUri()).build(); + } + + AuthorizationCodeAuthorizationResponseAttributes authorizationCodeResponseAttributes = + this.authorizationCodeResponseConverter.convert(request); + + AuthorizationCodeAuthenticationToken authRequest = new AuthorizationCodeAuthenticationToken( + authorizationCodeResponseAttributes.getCode(), clientRegistration); + + authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); + + Authentication authenticated = this.getAuthenticationManager().authenticate(authRequest); + + return authenticated; + } + + public RequestMatcher getAuthorizeRequestMatcher() { + return this.authorizeRequestMatcher; + } + + protected ClientRegistrationRepository getClientRegistrationRepository() { + return this.clientRegistrationRepository; + } + + public final void setClientRegistrationRepository(ClientRegistrationRepository clientRegistrationRepository) { + Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); + Assert.notEmpty(clientRegistrationRepository.getRegistrations(), "clientRegistrationRepository cannot be empty"); + this.clientRegistrationRepository = clientRegistrationRepository; + } + + protected AuthorizationRequestRepository getAuthorizationRequestRepository() { + return this.authorizationRequestRepository; + } + + public final void setAuthorizationRequestRepository(AuthorizationRequestRepository authorizationRequestRepository) { + Assert.notNull(authorizationRequestRepository, "authorizationRequestRepository cannot be null"); + this.authorizationRequestRepository = authorizationRequestRepository; + } + + private AuthorizationRequestAttributes resolveAuthorizationRequest(HttpServletRequest request) { + AuthorizationRequestAttributes authorizationRequest = + this.getAuthorizationRequestRepository().loadAuthorizationRequest(request); + if (authorizationRequest == null) { + OAuth2Error oauth2Error = new OAuth2Error(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + } + this.getAuthorizationRequestRepository().removeAuthorizationRequest(request); + this.assertMatchingAuthorizationRequest(request, authorizationRequest); + return authorizationRequest; + } + + private void assertMatchingAuthorizationRequest(HttpServletRequest request, AuthorizationRequestAttributes authorizationRequest) { + String state = request.getParameter(OAuth2Parameter.STATE); + if (!authorizationRequest.getState().equals(state)) { + OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + } + + if (!request.getRequestURL().toString().equals(authorizationRequest.getRedirectUri())) { + OAuth2Error oauth2Error = new OAuth2Error(INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + } + } + + private static class ClientRegistrationBuilderWithUriOverrides extends ClientRegistration.Builder { + + private ClientRegistrationBuilderWithUriOverrides(ClientRegistration clientRegistration, String redirectUri) { + super(clientRegistration.getClientId()); + this.clientSecret(clientRegistration.getClientSecret()); + this.clientAuthenticationMethod(clientRegistration.getClientAuthenticationMethod()); + this.authorizedGrantType(clientRegistration.getAuthorizedGrantType()); + this.redirectUri(redirectUri); + if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) { + this.scopes(clientRegistration.getScopes().stream().toArray(String[]::new)); + } + this.authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri()); + this.tokenUri(clientRegistration.getProviderDetails().getTokenUri()); + this.userInfoUri(clientRegistration.getProviderDetails().getUserInfoUri()); + this.clientName(clientRegistration.getClientName()); + this.clientAlias(clientRegistration.getClientAlias()); + } + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProvider.java new file mode 100644 index 0000000000..4ae4fd1665 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProvider.java @@ -0,0 +1,120 @@ +/* + * Copyright 2012-2017 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.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper; +import org.springframework.security.oauth2.client.user.OAuth2UserService; +import org.springframework.security.oauth2.core.AccessToken; +import org.springframework.security.oauth2.core.endpoint.TokenResponseAttributes; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.util.Assert; + +import java.util.Collection; + +/** + * An implementation of an {@link AuthenticationProvider} that 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. + * Additionally, it will also obtain the end-user's (resource owner) attributes from the UserInfo Endpoint + * (using the access token) and create a Principal in the form of an {@link OAuth2User} + * associating it with the returned {@link OAuth2AuthenticationToken}. + * + *

+ * The {@link AuthorizationCodeAuthenticationProvider} uses an {@link AuthorizationGrantTokenExchanger} + * to make a request to the authorization server's Token Endpoint + * to verify the {@link AuthorizationCodeAuthenticationToken#getAuthorizationCode()}. + * If the request is valid, the authorization server will respond back with a {@link TokenResponseAttributes}. + * + *

+ * It will then create a {@link OAuth2AuthenticationToken} associating the {@link AccessToken} + * from the {@link TokenResponseAttributes} and pass it to {@link OAuth2UserService#loadUser(OAuth2AuthenticationToken)} + * to obtain the end-user's (resource owner) attributes in the form of an {@link OAuth2User}. + * + *

+ * Finally, it will create another {@link OAuth2AuthenticationToken}, this time associating + * the {@link AccessToken} and {@link OAuth2User} and return it to the {@link AuthenticationManager}, + * at which point the {@link OAuth2AuthenticationToken} is considered "authenticated". + * + * @author Joe Grandja + * @since 5.0 + * @see AuthorizationCodeAuthenticationToken + * @see AuthorizationGrantTokenExchanger + * @see TokenResponseAttributes + * @see AccessToken + * @see OAuth2UserService + * @see OAuth2User + * @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 AuthorizationCodeAuthenticationProvider implements AuthenticationProvider { + private final AuthorizationGrantTokenExchanger authorizationCodeTokenExchanger; + private final OAuth2UserService userInfoService; + private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); + + public AuthorizationCodeAuthenticationProvider( + AuthorizationGrantTokenExchanger authorizationCodeTokenExchanger, + OAuth2UserService userInfoService) { + + Assert.notNull(authorizationCodeTokenExchanger, "authorizationCodeTokenExchanger cannot be null"); + Assert.notNull(userInfoService, "userInfoService cannot be null"); + this.authorizationCodeTokenExchanger = authorizationCodeTokenExchanger; + this.userInfoService = userInfoService; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + AuthorizationCodeAuthenticationToken authorizationCodeAuthentication = + (AuthorizationCodeAuthenticationToken) authentication; + + TokenResponseAttributes tokenResponse = + this.authorizationCodeTokenExchanger.exchange(authorizationCodeAuthentication); + + AccessToken accessToken = new AccessToken(tokenResponse.getTokenType(), + tokenResponse.getTokenValue(), tokenResponse.getIssuedAt(), + tokenResponse.getExpiresAt(), tokenResponse.getScopes()); + OAuth2AuthenticationToken accessTokenAuthentication = new OAuth2AuthenticationToken( + authorizationCodeAuthentication.getClientRegistration(), accessToken); + accessTokenAuthentication.setDetails(authorizationCodeAuthentication.getDetails()); + + OAuth2User user = this.userInfoService.loadUser(accessTokenAuthentication); + + Collection authorities = + this.authoritiesMapper.mapAuthorities(user.getAuthorities()); + + OAuth2AuthenticationToken authenticationResult = new OAuth2AuthenticationToken(user, authorities, + accessTokenAuthentication.getClientRegistration(), accessTokenAuthentication.getAccessToken()); + authenticationResult.setDetails(accessTokenAuthentication.getDetails()); + + return authenticationResult; + } + + @Override + public boolean supports(Class authentication) { + return AuthorizationCodeAuthenticationToken.class.isAssignableFrom(authentication); + } + + public final void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) { + Assert.notNull(authoritiesMapper, "authoritiesMapper cannot be null"); + this.authoritiesMapper = authoritiesMapper; + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationToken.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationToken.java new file mode 100644 index 0000000000..6a510d5ff1 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationToken.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2017 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.core.authority.AuthorityUtils; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.util.Assert; + +/** + * An implementation of an {@link AuthorizationGrantAuthenticationToken} that holds + * an authorization code grant credential for a specific client identified in {@link #getClientRegistration()}. + * + * @author Joe Grandja + * @since 5.0 + * @see AuthorizationGrantAuthenticationToken + * @see ClientRegistration + * @see Section 1.3.1 Authorization Code Grant + */ +public class AuthorizationCodeAuthenticationToken extends AuthorizationGrantAuthenticationToken { + private final String authorizationCode; + private final ClientRegistration clientRegistration; + + public AuthorizationCodeAuthenticationToken(String authorizationCode, ClientRegistration clientRegistration) { + super(AuthorizationGrantType.AUTHORIZATION_CODE, AuthorityUtils.NO_AUTHORITIES); + Assert.hasText(authorizationCode, "authorizationCode cannot be empty"); + Assert.notNull(clientRegistration, "clientRegistration cannot be null"); + this.authorizationCode = authorizationCode; + this.clientRegistration = clientRegistration; + this.setAuthenticated(false); + } + + @Override + public Object getPrincipal() { + return null; + } + + @Override + public Object getCredentials() { + return this.getAuthorizationCode(); + } + + public String getAuthorizationCode() { + return this.authorizationCode; + } + + public ClientRegistration getClientRegistration() { + return this.clientRegistration; + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeRequestRedirectFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeRequestRedirectFilter.java new file mode 100644 index 0000000000..48226963c8 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeRequestRedirectFilter.java @@ -0,0 +1,159 @@ +/* + * Copyright 2012-2017 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.crypto.keygen.StringKeyGenerator; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.core.endpoint.AuthorizationRequestAttributes; +import org.springframework.security.web.DefaultRedirectStrategy; +import org.springframework.security.web.RedirectStrategy; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.util.Assert; +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; +import java.net.URI; + +import static org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationProcessingFilter.AUTHORIZE_BASE_URI; + +/** + * This Filter initiates the authorization code grant flow by redirecting + * the end-user's user-agent to the authorization server's Authorization Endpoint. + * + *

+ * It uses an {@link AuthorizationRequestUriBuilder} to build the OAuth 2.0 Authorization Request, + * which is used as the redirect URI to the Authorization Endpoint. + * The redirect URI will include the client identifier, requested scope(s), state, response type, and a redirection URI + * which the authorization server will send the user-agent back to (handled by {@link AuthorizationCodeAuthenticationProcessingFilter}) + * once access is granted (or denied) by the end-user (resource owner). + * + * @author Joe Grandja + * @since 5.0 + * @see AuthorizationRequestAttributes + * @see AuthorizationRequestRepository + * @see AuthorizationRequestUriBuilder + * @see ClientRegistration + * @see ClientRegistrationRepository + * @see AuthorizationCodeAuthenticationProcessingFilter + * @see Section 4.1 Authorization Code Grant Flow + * @see Section 4.1.1 Authorization Request + */ +public class AuthorizationCodeRequestRedirectFilter extends OncePerRequestFilter { + public static final String AUTHORIZATION_BASE_URI = "/oauth2/authorization/code"; + private static final String CLIENT_ALIAS_VARIABLE_NAME = "clientAlias"; + private static final String AUTHORIZATION_URI = AUTHORIZATION_BASE_URI + "/{" + CLIENT_ALIAS_VARIABLE_NAME + "}"; + private static final String DEFAULT_REDIRECT_URI_TEMPLATE = "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"; + private final AntPathRequestMatcher authorizationRequestMatcher; + private final ClientRegistrationRepository clientRegistrationRepository; + private final AuthorizationRequestUriBuilder authorizationUriBuilder; + private final RedirectStrategy authorizationRedirectStrategy = new DefaultRedirectStrategy(); + private final StringKeyGenerator stateGenerator = new DefaultStateGenerator(); + private AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionAuthorizationRequestRepository(); + + public AuthorizationCodeRequestRedirectFilter(ClientRegistrationRepository clientRegistrationRepository, + AuthorizationRequestUriBuilder authorizationUriBuilder) { + + Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); + Assert.notNull(authorizationUriBuilder, "authorizationUriBuilder cannot be null"); + this.authorizationRequestMatcher = new AntPathRequestMatcher(AUTHORIZATION_URI); + this.clientRegistrationRepository = clientRegistrationRepository; + this.authorizationUriBuilder = authorizationUriBuilder; + } + + 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.requiresAuthorization(request, response)) { + try { + this.sendRedirectForAuthorization(request, response); + } catch (Exception failed) { + this.unsuccessfulAuthorization(request, response, failed); + } + return; + } + + filterChain.doFilter(request, response); + } + + protected boolean requiresAuthorization(HttpServletRequest request, HttpServletResponse response) { + return this.authorizationRequestMatcher.matches(request); + } + + protected void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + + String clientAlias = this.authorizationRequestMatcher + .extractUriTemplateVariables(request).get(CLIENT_ALIAS_VARIABLE_NAME); + ClientRegistration clientRegistration = this.clientRegistrationRepository.getRegistrationByClientAlias(clientAlias); + if (clientRegistration == null) { + throw new IllegalArgumentException("Invalid Client Identifier (Alias): " + clientAlias); + } + + String redirectUriStr; + if (isDefaultRedirectUri(clientRegistration)) { + redirectUriStr = this.expandDefaultRedirectUri(request, clientRegistration); + } else { + redirectUriStr = clientRegistration.getRedirectUri(); + } + + AuthorizationRequestAttributes authorizationRequestAttributes = + AuthorizationRequestAttributes.withAuthorizationCode() + .clientId(clientRegistration.getClientId()) + .authorizeUri(clientRegistration.getProviderDetails().getAuthorizationUri()) + .redirectUri(redirectUriStr) + .scopes(clientRegistration.getScopes()) + .state(this.stateGenerator.generateKey()) + .build(); + + this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequestAttributes, request); + + URI redirectUri = this.authorizationUriBuilder.build(authorizationRequestAttributes); + this.authorizationRedirectStrategy.sendRedirect(request, response, redirectUri.toString()); + } + + protected void unsuccessfulAuthorization(HttpServletRequest request, HttpServletResponse response, + Exception failed) throws IOException, ServletException { + + if (logger.isDebugEnabled()) { + logger.debug("Authorization Request failed: " + failed.toString(), failed); + } + response.sendError(HttpServletResponse.SC_BAD_REQUEST, failed.getMessage()); + } + + static boolean isDefaultRedirectUri(ClientRegistration clientRegistration) { + return DEFAULT_REDIRECT_URI_TEMPLATE.equals(clientRegistration.getRedirectUri()); + } + + private String expandDefaultRedirectUri(HttpServletRequest request, ClientRegistration clientRegistration) { + return UriComponentsBuilder.fromUriString(DEFAULT_REDIRECT_URI_TEMPLATE) + .buildAndExpand(request.getScheme(), request.getServerName(), request.getServerPort(), + AUTHORIZE_BASE_URI, clientRegistration.getClientAlias()) + .encode() + .toUriString(); + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationGrantAuthenticationToken.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationGrantAuthenticationToken.java new file mode 100644 index 0000000000..2e522c936e --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationGrantAuthenticationToken.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2017 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.GrantedAuthority; +import org.springframework.security.core.SpringSecurityCoreVersion; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.util.Assert; + +import java.util.Collection; + +/** + * Base implementation of an {@link AbstractAuthenticationToken} that holds + * an authorization grant credential for a specific {@link AuthorizationGrantType}. + * + * @author Joe Grandja + * @since 5.0 + * @see AuthorizationGrantType + * @see Section 1.3 Authorization Grant + */ +public abstract class AuthorizationGrantAuthenticationToken extends AbstractAuthenticationToken { + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + private final AuthorizationGrantType authorizationGrantType; + + protected AuthorizationGrantAuthenticationToken(AuthorizationGrantType authorizationGrantType, + Collection authorities) { + + super(authorities); + Assert.notNull(authorizationGrantType, "authorizationGrantType cannot be null"); + this.authorizationGrantType = authorizationGrantType; + } + + public AuthorizationGrantType getGrantType() { + return this.authorizationGrantType; + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationGrantTokenExchanger.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationGrantTokenExchanger.java new file mode 100644 index 0000000000..b5980bde6b --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationGrantTokenExchanger.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2017 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.AuthorizationGrantType; +import org.springframework.security.oauth2.core.endpoint.TokenResponseAttributes; + +/** + * Implementations of this interface are responsible for "exchanging" + * an authorization grant credential (for example, an authorization code) for an + * access token credential at the authorization server's Token Endpoint. + * + * @author Joe Grandja + * @since 5.0 + * @see AuthorizationGrantType + * @see AuthorizationGrantAuthenticationToken + * @see TokenResponseAttributes + * @see Section 1.3 Authorization Grant + * @see Section 4.1.3 Access Token Request (Authorization Code Grant) + * @see Section 4.1.4 Access Token Response (Authorization Code Grant) + */ +public interface AuthorizationGrantTokenExchanger { + + TokenResponseAttributes exchange(T authorizationGrantAuthentication) throws OAuth2AuthenticationException; + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationRequestRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationRequestRepository.java new file mode 100644 index 0000000000..3954a03bb5 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationRequestRepository.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2017 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.endpoint.AuthorizationRequestAttributes; + +import javax.servlet.http.HttpServletRequest; + +/** + * Implementations of this interface are responsible for the persistence + * of {@link AuthorizationRequestAttributes} between requests. + * + *

+ * Used by the {@link AuthorizationCodeRequestRedirectFilter} for persisting the Authorization Request + * before it initiates the authorization code grant flow. + * As well, used by the {@link AuthorizationCodeAuthenticationProcessingFilter} when resolving + * the associated Authorization Request during the handling of the Authorization Response. + * + * @author Joe Grandja + * @since 5.0 + * @see AuthorizationRequestAttributes + * @see HttpSessionAuthorizationRequestRepository + */ +public interface AuthorizationRequestRepository { + + AuthorizationRequestAttributes loadAuthorizationRequest(HttpServletRequest request); + + void saveAuthorizationRequest(AuthorizationRequestAttributes authorizationRequest, HttpServletRequest request); + + AuthorizationRequestAttributes removeAuthorizationRequest(HttpServletRequest request); + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationRequestUriBuilder.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationRequestUriBuilder.java new file mode 100644 index 0000000000..2eba15386c --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationRequestUriBuilder.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2017 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.endpoint.AuthorizationRequestAttributes; + +import java.net.URI; + +/** + * + * @author Joe Grandja + * @since 5.0 + */ +public interface AuthorizationRequestUriBuilder { + + URI build(AuthorizationRequestAttributes authorizationRequestAttributes); +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/DefaultAuthorizationRequestUriBuilder.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/DefaultAuthorizationRequestUriBuilder.java new file mode 100644 index 0000000000..e2ae810873 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/DefaultAuthorizationRequestUriBuilder.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2017 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.endpoint.OAuth2Parameter; +import org.springframework.security.oauth2.core.endpoint.ResponseType; +import org.springframework.security.oauth2.core.endpoint.AuthorizationRequestAttributes; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; +import java.util.stream.Collectors; + +/** + * + * @author Joe Grandja + * @since 5.0 + */ +public class DefaultAuthorizationRequestUriBuilder implements AuthorizationRequestUriBuilder { + + @Override + public URI build(AuthorizationRequestAttributes authorizationRequestAttributes) { + UriComponentsBuilder uriBuilder = UriComponentsBuilder + .fromUriString(authorizationRequestAttributes.getAuthorizeUri()) + .queryParam(OAuth2Parameter.RESPONSE_TYPE, ResponseType.CODE.value()); + if (authorizationRequestAttributes.getRedirectUri() != null) { + uriBuilder.queryParam(OAuth2Parameter.REDIRECT_URI, authorizationRequestAttributes.getRedirectUri()); + } + uriBuilder + .queryParam(OAuth2Parameter.CLIENT_ID, authorizationRequestAttributes.getClientId()) + .queryParam(OAuth2Parameter.SCOPE, + authorizationRequestAttributes.getScopes().stream().collect(Collectors.joining(" "))) + .queryParam(OAuth2Parameter.STATE, authorizationRequestAttributes.getState()); + + return uriBuilder.build().encode().toUri(); + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/DefaultStateGenerator.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/DefaultStateGenerator.java new file mode 100644 index 0000000000..095ba53bd7 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/DefaultStateGenerator.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2017 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.crypto.codec.Base64; +import org.springframework.security.crypto.keygen.BytesKeyGenerator; +import org.springframework.security.crypto.keygen.KeyGenerators; +import org.springframework.security.crypto.keygen.StringKeyGenerator; +import org.springframework.util.Assert; + +/** + * The default implementation for generating the + * {@link org.springframework.security.oauth2.core.endpoint.OAuth2Parameter#STATE} parameter + * used in the Authorization Request and correlated in the Authorization Response (or Error Response). + * + *

+ * NOTE: The value of the state parameter is an opaque String + * used by the client to prevent cross-site request forgery, as described in + * Section 10.12 of the specification. + * + * @author Joe Grandja + * @since 5.0 + */ +public class DefaultStateGenerator implements StringKeyGenerator { + private static final int DEFAULT_BYTE_LENGTH = 32; + private final BytesKeyGenerator keyGenerator; + + public DefaultStateGenerator() { + this(DEFAULT_BYTE_LENGTH); + } + + public DefaultStateGenerator(int byteLength) { + Assert.isTrue(byteLength > 0, "byteLength must be greater than 0"); + this.keyGenerator = KeyGenerators.secureRandom(byteLength); + } + + @Override + public String generateKey() { + return new String(Base64.encode(keyGenerator.generateKey())); + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/HttpSessionAuthorizationRequestRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/HttpSessionAuthorizationRequestRepository.java new file mode 100644 index 0000000000..dfc343d9b0 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/HttpSessionAuthorizationRequestRepository.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2017 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.endpoint.AuthorizationRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; + +/** + * An implementation of an {@link AuthorizationRequestRepository} that stores + * {@link AuthorizationRequestAttributes} in the {@link HttpSession}. + * + * @author Joe Grandja + * @since 5.0 + * @see AuthorizationRequestAttributes + */ +public final class HttpSessionAuthorizationRequestRepository implements AuthorizationRequestRepository { + private static final String DEFAULT_AUTHORIZATION_REQUEST_ATTR_NAME = + HttpSessionAuthorizationRequestRepository.class.getName() + ".AUTHORIZATION_REQUEST"; + private String sessionAttributeName = DEFAULT_AUTHORIZATION_REQUEST_ATTR_NAME; + + @Override + public AuthorizationRequestAttributes loadAuthorizationRequest(HttpServletRequest request) { + AuthorizationRequestAttributes authorizationRequest = null; + HttpSession session = request.getSession(false); + if (session != null) { + authorizationRequest = (AuthorizationRequestAttributes) session.getAttribute(this.sessionAttributeName); + } + return authorizationRequest; + } + + @Override + public void saveAuthorizationRequest(AuthorizationRequestAttributes authorizationRequest, HttpServletRequest request) { + if (authorizationRequest == null) { + this.removeAuthorizationRequest(request); + return; + } + request.getSession().setAttribute(this.sessionAttributeName, authorizationRequest); + } + + @Override + public AuthorizationRequestAttributes removeAuthorizationRequest(HttpServletRequest request) { + AuthorizationRequestAttributes authorizationRequest = this.loadAuthorizationRequest(request); + if (authorizationRequest != null) { + request.getSession().removeAttribute(this.sessionAttributeName); + } + return authorizationRequest; + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationException.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationException.java new file mode 100644 index 0000000000..43465923d5 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationException.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-2017 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.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.util.Assert; + +/** + * This exception is thrown for all OAuth 2.0 related {@link Authentication} errors. + * + *

+ * There are a number of scenarios where an error may occur, for example: + *

    + *
  • The authorization request or token request is missing a required parameter
  • + *
  • Missing or invalid client identifier
  • + *
  • Invalid or mismatching redirection URI
  • + *
  • The requested scope is invalid, unknown, or malformed
  • + *
  • The resource owner or authorization server denied the access request
  • + *
  • Client authentication failed
  • + *
  • The provided authorization grant (authorization code, resource owner credentials) is invalid, expired, or revoked
  • + *
+ * + * @author Joe Grandja + * @since 5.0 + */ +public class OAuth2AuthenticationException extends AuthenticationException { + private OAuth2Error errorObject; + + public OAuth2AuthenticationException(OAuth2Error errorObject, Throwable cause) { + this(errorObject, cause.getMessage(), cause); + } + + public OAuth2AuthenticationException(OAuth2Error errorObject, String message) { + super(message); + this.setErrorObject(errorObject); + } + + public OAuth2AuthenticationException(OAuth2Error errorObject, String message, Throwable cause) { + super(message, cause); + this.setErrorObject(errorObject); + } + + public OAuth2Error getErrorObject() { + return errorObject; + } + + private void setErrorObject(OAuth2Error errorObject) { + Assert.notNull(errorObject, "OAuth2 Error object cannot be null"); + this.errorObject = errorObject; + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationToken.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationToken.java new file mode 100644 index 0000000000..d5119e3312 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationToken.java @@ -0,0 +1,87 @@ +/* + * Copyright 2012-2017 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.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.SpringSecurityCoreVersion; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.user.OAuth2UserService; +import org.springframework.security.oauth2.core.AccessToken; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.util.Assert; + +import java.util.Collection; + +/** + * An implementation of an {@link AbstractAuthenticationToken} + * that represents an OAuth 2.0 {@link Authentication}. + * + *

+ * It associates an {@link OAuth2User}, {@link ClientRegistration} and an {@link AccessToken}. + * This Authentication is considered "authenticated" if the {@link OAuth2User} + * is provided in the respective constructor. This typically happens after the {@link OAuth2UserService} + * retrieves the end-user's (resource owner) attributes from the UserInfo Endpoint. + * + * @author Joe Grandja + * @since 5.0 + * @see OAuth2User + * @see ClientRegistration + * @see AccessToken + */ +public class OAuth2AuthenticationToken extends AbstractAuthenticationToken { + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + private final OAuth2User principal; + private final ClientRegistration clientRegistration; + private final AccessToken accessToken; + + public OAuth2AuthenticationToken(ClientRegistration clientRegistration, AccessToken accessToken) { + this(null, AuthorityUtils.NO_AUTHORITIES, clientRegistration, accessToken); + } + + public OAuth2AuthenticationToken(OAuth2User principal, Collection authorities, + ClientRegistration clientRegistration, AccessToken accessToken) { + + super(authorities); + Assert.notNull(clientRegistration, "clientRegistration cannot be null"); + Assert.notNull(accessToken, "accessToken cannot be null"); + this.principal = principal; + this.clientRegistration = clientRegistration; + this.accessToken = accessToken; + this.setAuthenticated(principal != null); + } + + @Override + public Object getPrincipal() { + return this.principal; + } + + @Override + public Object getCredentials() { + // Credentials are never exposed (by the Provider) for an OAuth2 User + return ""; + } + + public ClientRegistration getClientRegistration() { + return this.clientRegistration; + } + + public AccessToken getAccessToken() { + return this.accessToken; + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/nimbus/NimbusAuthorizationCodeTokenExchanger.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/nimbus/NimbusAuthorizationCodeTokenExchanger.java new file mode 100644 index 0000000000..3b73d945b8 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/nimbus/NimbusAuthorizationCodeTokenExchanger.java @@ -0,0 +1,142 @@ +/* + * Copyright 2012-2017 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.nimbus; + + +import com.nimbusds.oauth2.sdk.*; +import com.nimbusds.oauth2.sdk.auth.ClientAuthentication; +import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic; +import com.nimbusds.oauth2.sdk.auth.ClientSecretPost; +import com.nimbusds.oauth2.sdk.auth.Secret; +import com.nimbusds.oauth2.sdk.http.HTTPRequest; +import com.nimbusds.oauth2.sdk.id.ClientID; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationToken; +import org.springframework.security.oauth2.client.authentication.AuthorizationGrantTokenExchanger; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationException; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AccessToken; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.endpoint.TokenResponseAttributes; +import org.springframework.util.CollectionUtils; + +import java.io.IOException; +import java.net.URI; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * An implementation of an {@link AuthorizationGrantTokenExchanger} that "exchanges" + * an authorization code credential for an access token credential + * at the authorization server's Token Endpoint. + * + *

+ * NOTE: This implementation uses the Nimbus OAuth 2.0 SDK internally. + * + * @author Joe Grandja + * @since 5.0 + * @see AuthorizationCodeAuthenticationToken + * @see TokenResponseAttributes + * @see Nimbus OAuth 2.0 SDK + * @see Section 4.1.3 Access Token Request (Authorization Code Grant) + * @see Section 4.1.4 Access Token Response (Authorization Code Grant) + */ +public class NimbusAuthorizationCodeTokenExchanger implements AuthorizationGrantTokenExchanger { + private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response"; + + @Override + public TokenResponseAttributes exchange(AuthorizationCodeAuthenticationToken authorizationCodeAuthenticationToken) + throws OAuth2AuthenticationException { + + ClientRegistration clientRegistration = authorizationCodeAuthenticationToken.getClientRegistration(); + + // Build the authorization code grant request for the token endpoint + AuthorizationCode authorizationCode = new AuthorizationCode(authorizationCodeAuthenticationToken.getAuthorizationCode()); + URI redirectUri = this.toURI(clientRegistration.getRedirectUri()); + AuthorizationGrant authorizationCodeGrant = new AuthorizationCodeGrant(authorizationCode, redirectUri); + URI tokenUri = this.toURI(clientRegistration.getProviderDetails().getTokenUri()); + + // Set the credentials to authenticate the client at the token endpoint + ClientID clientId = new ClientID(clientRegistration.getClientId()); + Secret clientSecret = new Secret(clientRegistration.getClientSecret()); + ClientAuthentication clientAuthentication; + if (ClientAuthenticationMethod.FORM.equals(clientRegistration.getClientAuthenticationMethod())) { + clientAuthentication = new ClientSecretPost(clientId, clientSecret); + } else { + clientAuthentication = new ClientSecretBasic(clientId, clientSecret); + } + + TokenResponse tokenResponse; + try { + // Send the Access Token request + TokenRequest tokenRequest = new TokenRequest(tokenUri, clientAuthentication, authorizationCodeGrant); + HTTPRequest httpRequest = tokenRequest.toHTTPRequest(); + httpRequest.setAccept(MediaType.APPLICATION_JSON_VALUE); + tokenResponse = TokenResponse.parse(httpRequest.send()); + } catch (ParseException pe) { + // This error occurs if the Access Token Response is not well-formed, + // for example, a required attribute is missing + throw new OAuth2AuthenticationException(new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE), pe); + } catch (IOException ioe) { + // This error occurs when there is a network-related issue + throw new AuthenticationServiceException("An error occurred while sending the Access Token Request: " + + ioe.getMessage(), ioe); + } + + if (!tokenResponse.indicatesSuccess()) { + TokenErrorResponse tokenErrorResponse = (TokenErrorResponse) tokenResponse; + ErrorObject errorObject = tokenErrorResponse.getErrorObject(); + OAuth2Error oauth2Error = new OAuth2Error(errorObject.getCode(), errorObject.getDescription(), + (errorObject.getURI() != null ? errorObject.getURI().toString() : null)); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + } + + AccessTokenResponse accessTokenResponse = (AccessTokenResponse) tokenResponse; + + String accessToken = accessTokenResponse.getTokens().getAccessToken().getValue(); + AccessToken.TokenType accessTokenType = null; + if (AccessToken.TokenType.BEARER.value().equals(accessTokenResponse.getTokens().getAccessToken().getType().getValue())) { + accessTokenType = AccessToken.TokenType.BEARER; + } + long expiresIn = accessTokenResponse.getTokens().getAccessToken().getLifetime(); + Set scopes = Collections.emptySet(); + if (!CollectionUtils.isEmpty(accessTokenResponse.getTokens().getAccessToken().getScope())) { + scopes = new HashSet<>(accessTokenResponse.getTokens().getAccessToken().getScope().toStringList()); + } + Map additionalParameters = accessTokenResponse.getCustomParameters().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + return TokenResponseAttributes.withToken(accessToken) + .tokenType(accessTokenType) + .expiresIn(expiresIn) + .scopes(scopes) + .additionalParameters(additionalParameters) + .build(); + } + + private URI toURI(String uriStr) { + try { + return new URI(uriStr); + } catch (Exception ex) { + throw new IllegalArgumentException("An error occurred parsing URI: " + uriStr, ex); + } + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java new file mode 100644 index 0000000000..da5b3b2ad3 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java @@ -0,0 +1,279 @@ +/* + * Copyright 2012-2017 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.registration; + +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + + +/** + * + * @author Joe Grandja + * @since 5.0 + */ +public class ClientRegistration { + private String clientId; + private String clientSecret; + private ClientAuthenticationMethod clientAuthenticationMethod = ClientAuthenticationMethod.HEADER; + private AuthorizationGrantType authorizedGrantType; + private String redirectUri; + private Set scopes = Collections.emptySet(); + private ProviderDetails providerDetails = new ProviderDetails(); + private String clientName; + private String clientAlias; + + protected ClientRegistration() { + } + + public String getClientId() { + return this.clientId; + } + + protected void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getClientSecret() { + return this.clientSecret; + } + + protected void setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + } + + public ClientAuthenticationMethod getClientAuthenticationMethod() { + return this.clientAuthenticationMethod; + } + + protected void setClientAuthenticationMethod(ClientAuthenticationMethod clientAuthenticationMethod) { + this.clientAuthenticationMethod = clientAuthenticationMethod; + } + + public AuthorizationGrantType getAuthorizedGrantType() { + return this.authorizedGrantType; + } + + protected void setAuthorizedGrantType(AuthorizationGrantType authorizedGrantType) { + this.authorizedGrantType = authorizedGrantType; + } + + public String getRedirectUri() { + return this.redirectUri; + } + + protected void setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; + } + + public Set getScopes() { + return this.scopes; + } + + protected void setScopes(Set scopes) { + this.scopes = scopes; + } + + public ProviderDetails getProviderDetails() { + return this.providerDetails; + } + + protected void setProviderDetails(ProviderDetails providerDetails) { + this.providerDetails = providerDetails; + } + + public String getClientName() { + return this.clientName; + } + + protected void setClientName(String clientName) { + this.clientName = clientName; + } + + public String getClientAlias() { + return this.clientAlias; + } + + protected void setClientAlias(String clientAlias) { + this.clientAlias = clientAlias; + } + + public class ProviderDetails { + private String authorizationUri; + private String tokenUri; + private String userInfoUri; + + protected ProviderDetails() { + } + + public String getAuthorizationUri() { + return this.authorizationUri; + } + + protected void setAuthorizationUri(String authorizationUri) { + this.authorizationUri = authorizationUri; + } + + public String getTokenUri() { + return this.tokenUri; + } + + protected void setTokenUri(String tokenUri) { + this.tokenUri = tokenUri; + } + + public String getUserInfoUri() { + return this.userInfoUri; + } + + protected void setUserInfoUri(String userInfoUri) { + this.userInfoUri = userInfoUri; + } + } + + public static class Builder { + protected String clientId; + protected String clientSecret; + protected ClientAuthenticationMethod clientAuthenticationMethod = ClientAuthenticationMethod.HEADER; + protected AuthorizationGrantType authorizedGrantType; + protected String redirectUri; + protected Set scopes; + protected String authorizationUri; + protected String tokenUri; + protected String userInfoUri; + protected String clientName; + protected String clientAlias; + + public Builder(String clientId) { + this.clientId = clientId; + } + + public Builder(ClientRegistrationProperties clientRegistrationProperties) { + this(clientRegistrationProperties.getClientId()); + this.clientSecret(clientRegistrationProperties.getClientSecret()); + this.clientAuthenticationMethod(clientRegistrationProperties.getClientAuthenticationMethod()); + this.authorizedGrantType(clientRegistrationProperties.getAuthorizedGrantType()); + this.redirectUri(clientRegistrationProperties.getRedirectUri()); + if (!CollectionUtils.isEmpty(clientRegistrationProperties.getScopes())) { + this.scopes(clientRegistrationProperties.getScopes().stream().toArray(String[]::new)); + } + this.authorizationUri(clientRegistrationProperties.getAuthorizationUri()); + this.tokenUri(clientRegistrationProperties.getTokenUri()); + this.userInfoUri(clientRegistrationProperties.getUserInfoUri()); + this.clientName(clientRegistrationProperties.getClientName()); + this.clientAlias(clientRegistrationProperties.getClientAlias()); + } + + public Builder clientSecret(String clientSecret) { + this.clientSecret = clientSecret; + return this; + } + + public Builder clientAuthenticationMethod(ClientAuthenticationMethod clientAuthenticationMethod) { + this.clientAuthenticationMethod = clientAuthenticationMethod; + return this; + } + + public Builder authorizedGrantType(AuthorizationGrantType authorizedGrantType) { + this.authorizedGrantType = authorizedGrantType; + return this; + } + + public Builder redirectUri(String redirectUri) { + this.redirectUri = redirectUri; + return this; + } + + public Builder scopes(String... scopes) { + if (scopes != null && scopes.length > 0) { + this.scopes = Collections.unmodifiableSet( + new LinkedHashSet<>(Arrays.asList(scopes))); + } + return this; + } + + public Builder authorizationUri(String authorizationUri) { + this.authorizationUri = authorizationUri; + return this; + } + + public Builder tokenUri(String tokenUri) { + this.tokenUri = tokenUri; + return this; + } + + public Builder userInfoUri(String userInfoUri) { + this.userInfoUri = userInfoUri; + return this; + } + + public Builder clientName(String clientName) { + this.clientName = clientName; + return this; + } + + public Builder clientAlias(String clientAlias) { + this.clientAlias = clientAlias; + return this; + } + + public ClientRegistration build() { + this.validateClientWithAuthorizationCodeGrantType(); + ClientRegistration clientRegistration = new ClientRegistration(); + this.setProperties(clientRegistration); + return clientRegistration; + } + + protected void setProperties(ClientRegistration clientRegistration) { + clientRegistration.setClientId(this.clientId); + clientRegistration.setClientSecret(this.clientSecret); + clientRegistration.setClientAuthenticationMethod(this.clientAuthenticationMethod); + clientRegistration.setAuthorizedGrantType(this.authorizedGrantType); + clientRegistration.setRedirectUri(this.redirectUri); + clientRegistration.setScopes(this.scopes); + + ProviderDetails providerDetails = clientRegistration.new ProviderDetails(); + providerDetails.setAuthorizationUri(this.authorizationUri); + providerDetails.setTokenUri(this.tokenUri); + providerDetails.setUserInfoUri(this.userInfoUri); + clientRegistration.setProviderDetails(providerDetails); + + clientRegistration.setClientName(this.clientName); + clientRegistration.setClientAlias(this.clientAlias); + } + + protected void validateClientWithAuthorizationCodeGrantType() { + Assert.isTrue(AuthorizationGrantType.AUTHORIZATION_CODE.equals(this.authorizedGrantType), + "authorizedGrantType must be " + AuthorizationGrantType.AUTHORIZATION_CODE.value()); + Assert.hasText(this.clientId, "clientId cannot be empty"); + Assert.hasText(this.clientSecret, "clientSecret cannot be empty"); + Assert.notNull(this.clientAuthenticationMethod, "clientAuthenticationMethod cannot be null"); + Assert.hasText(this.redirectUri, "redirectUri cannot be empty"); + Assert.notEmpty(this.scopes, "scopes cannot be empty"); + Assert.hasText(this.authorizationUri, "authorizationUri cannot be empty"); + Assert.hasText(this.tokenUri, "tokenUri cannot be empty"); + Assert.hasText(this.userInfoUri, "userInfoUri cannot be empty"); + Assert.hasText(this.clientName, "clientName cannot be empty"); + Assert.hasText(this.clientAlias, "clientAlias cannot be empty"); + } + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrationProperties.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrationProperties.java new file mode 100644 index 0000000000..a4c73fb3b7 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrationProperties.java @@ -0,0 +1,129 @@ +/* + * Copyright 2012-2017 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.registration; + +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; + +import java.util.Set; + +/** + * + * @author Joe Grandja + * @since 5.0 + */ +public class ClientRegistrationProperties { + private String clientId; + private String clientSecret; + private ClientAuthenticationMethod clientAuthenticationMethod = ClientAuthenticationMethod.HEADER; + private AuthorizationGrantType authorizedGrantType; + private String redirectUri; + private Set scopes; + private String authorizationUri; + private String tokenUri; + private String userInfoUri; + private String clientName; + private String clientAlias; + + + public String getClientId() { + return this.clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getClientSecret() { + return this.clientSecret; + } + + public void setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + } + + public ClientAuthenticationMethod getClientAuthenticationMethod() { + return this.clientAuthenticationMethod; + } + + public void setClientAuthenticationMethod(ClientAuthenticationMethod clientAuthenticationMethod) { + this.clientAuthenticationMethod = clientAuthenticationMethod; + } + + public AuthorizationGrantType getAuthorizedGrantType() { + return this.authorizedGrantType; + } + + public void setAuthorizedGrantType(AuthorizationGrantType authorizedGrantType) { + this.authorizedGrantType = authorizedGrantType; + } + + public String getRedirectUri() { + return this.redirectUri; + } + + public void setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; + } + + public Set getScopes() { + return this.scopes; + } + + public void setScopes(Set scopes) { + this.scopes = scopes; + } + + public String getAuthorizationUri() { + return this.authorizationUri; + } + + public void setAuthorizationUri(String authorizationUri) { + this.authorizationUri = authorizationUri; + } + + public String getTokenUri() { + return this.tokenUri; + } + + public void setTokenUri(String tokenUri) { + this.tokenUri = tokenUri; + } + + public String getUserInfoUri() { + return this.userInfoUri; + } + + public void setUserInfoUri(String userInfoUri) { + this.userInfoUri = userInfoUri; + } + + public String getClientName() { + return this.clientName; + } + + public void setClientName(String clientName) { + this.clientName = clientName; + } + + public String getClientAlias() { + return this.clientAlias; + } + + public void setClientAlias(String clientAlias) { + this.clientAlias = clientAlias; + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrationRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrationRepository.java new file mode 100644 index 0000000000..acfab16adf --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrationRepository.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2017 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.registration; + +import java.util.List; + +/** + * + * @author Joe Grandja + * @since 5.0 + */ +public interface ClientRegistrationRepository { + + ClientRegistration getRegistrationByClientId(String clientId); + + ClientRegistration getRegistrationByClientAlias(String clientAlias); + + List getRegistrations(); + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/InMemoryClientRegistrationRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/InMemoryClientRegistrationRepository.java new file mode 100644 index 0000000000..3cd21fa51a --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/InMemoryClientRegistrationRepository.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2017 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.registration; + +import org.springframework.util.Assert; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +/** + * + * @author Joe Grandja + * @since 5.0 + */ +public final class InMemoryClientRegistrationRepository implements ClientRegistrationRepository { + private final List clientRegistrations; + + public InMemoryClientRegistrationRepository(List clientRegistrations) { + Assert.notEmpty(clientRegistrations, "clientRegistrations cannot be empty"); + this.clientRegistrations = Collections.unmodifiableList(clientRegistrations); + } + + @Override + public ClientRegistration getRegistrationByClientId(String clientId) { + Optional clientRegistration = + this.clientRegistrations.stream() + .filter(c -> c.getClientId().equals(clientId)) + .findFirst(); + return clientRegistration.isPresent() ? clientRegistration.get() : null; + } + + @Override + public ClientRegistration getRegistrationByClientAlias(String clientAlias) { + Optional clientRegistration = + this.clientRegistrations.stream() + .filter(c -> c.getClientAlias().equals(clientAlias)) + .findFirst(); + return clientRegistration.isPresent() ? clientRegistration.get() : null; + } + + @Override + public List getRegistrations() { + return this.clientRegistrations; + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/OAuth2UserService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/OAuth2UserService.java new file mode 100644 index 0000000000..d4b9d8a964 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/OAuth2UserService.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2017 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.user; + +import org.springframework.security.core.AuthenticatedPrincipal; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationException; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.oauth2.oidc.user.UserInfo; + +/** + * Implementations of this interface are responsible for obtaining + * the end-user's (resource owner) attributes from the UserInfo Endpoint + * using the provided {@link OAuth2AuthenticationToken#getAccessToken()} + * and returning an {@link AuthenticatedPrincipal} in the form of an {@link OAuth2User} + * (for a standard OAuth 2.0 Provider) or {@link UserInfo} (for an OpenID Connect 1.0 Provider). + * + * @author Joe Grandja + * @since 5.0 + * @see OAuth2AuthenticationToken + * @see AuthenticatedPrincipal + * @see OAuth2User + * @see UserInfo + */ +public interface OAuth2UserService { + + OAuth2User loadUser(OAuth2AuthenticationToken token) throws OAuth2AuthenticationException; + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/AbstractOAuth2UserConverter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/AbstractOAuth2UserConverter.java new file mode 100644 index 0000000000..1c94df7299 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/AbstractOAuth2UserConverter.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2017 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.user.converter; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.io.IOException; +import java.util.Map; + +/** + * + * @author Joe Grandja + * @since 5.0 + */ +public abstract class AbstractOAuth2UserConverter implements Converter { + private final HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); + + protected AbstractOAuth2UserConverter() { + } + + @Override + public final T convert(ClientHttpResponse clientHttpResponse) { + Map userAttributes; + + try { + userAttributes = (Map) this.jackson2HttpMessageConverter.read(Map.class, clientHttpResponse); + } catch (IOException ex) { + throw new IllegalArgumentException("An error occurred reading the UserInfo response: " + ex.getMessage(), ex); + } + + return this.convert(userAttributes); + } + + protected abstract T convert(Map userAttributes); + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/CustomOAuth2UserConverter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/CustomOAuth2UserConverter.java new file mode 100644 index 0000000000..48f4d75ca7 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/CustomOAuth2UserConverter.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2017 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.user.converter; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.io.IOException; + +/** + * + * @author Joe Grandja + * @since 5.0 + */ +public final class CustomOAuth2UserConverter implements Converter { + private final HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); + private final Class customType; + + public CustomOAuth2UserConverter(Class customType) { + this.customType = customType; + } + + @Override + public T convert(ClientHttpResponse clientHttpResponse) { + T user; + + try { + user = (T) this.jackson2HttpMessageConverter.read(this.customType, clientHttpResponse); + } catch (IOException ex) { + throw new IllegalArgumentException("An error occurred reading the UserInfo response: " + ex.getMessage(), ex); + } + + return user; + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/OAuth2UserConverter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/OAuth2UserConverter.java new file mode 100644 index 0000000000..6db9b2bded --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/OAuth2UserConverter.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2017 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.user.converter; + +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.util.Assert; + +import java.util.Map; + +/** + * + * @author Joe Grandja + * @since 5.0 + */ +public final class OAuth2UserConverter extends AbstractOAuth2UserConverter { + private final String nameAttributeKey; + + public OAuth2UserConverter(String nameAttributeKey) { + Assert.hasText(nameAttributeKey, "nameAttributeKey cannot be empty"); + this.nameAttributeKey = nameAttributeKey; + } + + @Override + protected OAuth2User convert(Map userAttributes) { + return new DefaultOAuth2User(userAttributes, this.nameAttributeKey); + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/UserInfoConverter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/UserInfoConverter.java new file mode 100644 index 0000000000..481ac8c4d9 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/UserInfoConverter.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2017 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.user.converter; + +import org.springframework.security.oauth2.oidc.user.DefaultUserInfo; +import org.springframework.security.oauth2.oidc.user.UserInfo; + +import java.util.Map; + +/** + * + * @author Joe Grandja + * @since 5.0 + */ +public final class UserInfoConverter extends AbstractOAuth2UserConverter { + + @Override + protected UserInfo convert(Map userAttributes) { + return new DefaultUserInfo(userAttributes); + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/nimbus/NimbusClientHttpResponse.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/nimbus/NimbusClientHttpResponse.java new file mode 100644 index 0000000000..adac4c8a68 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/nimbus/NimbusClientHttpResponse.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2017 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.user.nimbus; + +import com.nimbusds.oauth2.sdk.http.HTTPResponse; +import org.springframework.http.HttpHeaders; +import org.springframework.http.client.AbstractClientHttpResponse; +import org.springframework.util.Assert; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; + +/** + * + * @author Joe Grandja + * @since 5.0 + */ +final class NimbusClientHttpResponse extends AbstractClientHttpResponse { + private final HTTPResponse httpResponse; + private final HttpHeaders headers; + + NimbusClientHttpResponse(HTTPResponse httpResponse) { + Assert.notNull(httpResponse, "httpResponse cannot be null"); + this.httpResponse = httpResponse; + this.headers = new HttpHeaders(); + this.headers.setAll(httpResponse.getHeaders()); + } + + @Override + public int getRawStatusCode() throws IOException { + return this.httpResponse.getStatusCode(); + } + + @Override + public String getStatusText() throws IOException { + return String.valueOf(this.getRawStatusCode()); + } + + @Override + public void close() { + } + + @Override + public InputStream getBody() throws IOException { + InputStream inputStream = new ByteArrayInputStream( + this.httpResponse.getContent().getBytes(Charset.forName("UTF-8"))); + return inputStream; + } + + @Override + public HttpHeaders getHeaders() { + return this.headers; + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/nimbus/NimbusOAuth2UserService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/nimbus/NimbusOAuth2UserService.java new file mode 100644 index 0000000000..bb5dbf1f7c --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/nimbus/NimbusOAuth2UserService.java @@ -0,0 +1,121 @@ +/* + * Copyright 2012-2017 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.user.nimbus; + +import com.nimbusds.oauth2.sdk.ErrorObject; +import com.nimbusds.oauth2.sdk.ParseException; +import com.nimbusds.oauth2.sdk.http.HTTPRequest; +import com.nimbusds.oauth2.sdk.http.HTTPResponse; +import com.nimbusds.oauth2.sdk.token.BearerAccessToken; +import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse; +import com.nimbusds.openid.connect.sdk.UserInfoRequest; +import org.springframework.core.convert.converter.Converter; +import org.springframework.http.MediaType; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.core.AuthenticatedPrincipal; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationException; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.user.OAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.oauth2.oidc.user.UserInfo; +import org.springframework.util.Assert; + +import java.io.IOException; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; + +/** + * An implementation of an {@link OAuth2UserService} that uses the Nimbus OAuth 2.0 SDK internally. + * + *

+ * This implementation uses a Map of Converter's keyed by URI. + * The URI represents the UserInfo Endpoint address and the mapped Converter + * is capable of converting the UserInfo Response to either an + * {@link OAuth2User} (for a standard OAuth 2.0 Provider) or + * {@link UserInfo} (for an OpenID Connect 1.0 Provider). + * + * @author Joe Grandja + * @since 5.0 + * @see OAuth2AuthenticationToken + * @see AuthenticatedPrincipal + * @see OAuth2User + * @see UserInfo + * @see Converter + * @see Nimbus OAuth 2.0 SDK + */ +public class NimbusOAuth2UserService implements OAuth2UserService { + private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response"; + private final Map> userInfoTypeConverters; + + public NimbusOAuth2UserService(Map> userInfoTypeConverters) { + Assert.notEmpty(userInfoTypeConverters, "userInfoTypeConverters cannot be empty"); + this.userInfoTypeConverters = new HashMap<>(userInfoTypeConverters); + } + + @Override + public OAuth2User loadUser(OAuth2AuthenticationToken token) throws OAuth2AuthenticationException { + OAuth2User user; + + try { + ClientRegistration clientRegistration = token.getClientRegistration(); + + URI userInfoUri; + try { + userInfoUri = new URI(clientRegistration.getProviderDetails().getUserInfoUri()); + } catch (Exception ex) { + throw new IllegalArgumentException("An error occurred parsing the userInfo URI: " + + clientRegistration.getProviderDetails().getUserInfoUri(), ex); + } + + Converter userInfoConverter = this.userInfoTypeConverters.get(userInfoUri); + if (userInfoConverter == null) { + throw new IllegalArgumentException("There is no available User Info converter for " + userInfoUri.toString()); + } + + BearerAccessToken accessToken = new BearerAccessToken(token.getAccessToken().getTokenValue()); + + // Request the User Info + UserInfoRequest userInfoRequest = new UserInfoRequest(userInfoUri, accessToken); + HTTPRequest httpRequest = userInfoRequest.toHTTPRequest(); + httpRequest.setAccept(MediaType.APPLICATION_JSON_VALUE); + HTTPResponse httpResponse = httpRequest.send(); + + if (httpResponse.getStatusCode() != HTTPResponse.SC_OK) { + UserInfoErrorResponse userInfoErrorResponse = UserInfoErrorResponse.parse(httpResponse); + ErrorObject errorObject = userInfoErrorResponse.getErrorObject(); + OAuth2Error oauth2Error = new OAuth2Error(errorObject.getCode(), errorObject.getDescription(), + (errorObject.getURI() != null ? errorObject.getURI().toString() : null)); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + } + + user = userInfoConverter.convert(new NimbusClientHttpResponse(httpResponse)); + + } catch (ParseException ex) { + // This error occurs if the User Info Response is not well-formed or invalid + throw new OAuth2AuthenticationException(new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE), ex); + } catch (IOException ex) { + // This error occurs when there is a network-related issue + throw new AuthenticationServiceException("An error occurred while sending the User Info Request: " + + ex.getMessage(), ex); + } + + return user; + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/converter/AuthorizationCodeAuthorizationResponseAttributesConverter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/converter/AuthorizationCodeAuthorizationResponseAttributesConverter.java new file mode 100644 index 0000000000..9a8fadc9fb --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/converter/AuthorizationCodeAuthorizationResponseAttributesConverter.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2017 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.converter; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.oauth2.core.endpoint.OAuth2Parameter; +import org.springframework.security.oauth2.core.endpoint.AuthorizationCodeAuthorizationResponseAttributes; +import org.springframework.util.Assert; + +import javax.servlet.http.HttpServletRequest; + +/** + * + * @author Joe Grandja + * @since 5.0 + */ +public final class AuthorizationCodeAuthorizationResponseAttributesConverter implements Converter { + + @Override + public AuthorizationCodeAuthorizationResponseAttributes convert(HttpServletRequest request) { + AuthorizationCodeAuthorizationResponseAttributes response; + + String code = request.getParameter(OAuth2Parameter.CODE); + Assert.hasText(code, OAuth2Parameter.CODE + " attribute is required"); + + String state = request.getParameter(OAuth2Parameter.STATE); + + response = new AuthorizationCodeAuthorizationResponseAttributes(code, state); + + return response; + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/converter/ErrorResponseAttributesConverter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/converter/ErrorResponseAttributesConverter.java new file mode 100644 index 0000000000..2c422e8246 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/converter/ErrorResponseAttributesConverter.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2017 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.converter; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.oauth2.core.endpoint.ErrorResponseAttributes; +import org.springframework.security.oauth2.core.endpoint.OAuth2Parameter; +import org.springframework.util.StringUtils; + +import javax.servlet.http.HttpServletRequest; + +/** + * + * @author Joe Grandja + * @since 5.0 + */ +public final class ErrorResponseAttributesConverter implements Converter { + + @Override + public ErrorResponseAttributes convert(HttpServletRequest request) { + ErrorResponseAttributes response; + + String errorCode = request.getParameter(OAuth2Parameter.ERROR); + if (!StringUtils.hasText(errorCode)) { + return null; + } + + String description = request.getParameter(OAuth2Parameter.ERROR_DESCRIPTION); + String uri = request.getParameter(OAuth2Parameter.ERROR_URI); + String state = request.getParameter(OAuth2Parameter.STATE); + + response = ErrorResponseAttributes.withErrorCode(errorCode) + .description(description) + .uri(uri) + .state(state) + .build(); + + return response; + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProcessingFilterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProcessingFilterTests.java new file mode 100644 index 0000000000..f7cd49b88e --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProcessingFilterTests.java @@ -0,0 +1,254 @@ +/* + * Copyright 2012-2017 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.Test; +import org.mockito.ArgumentCaptor; +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.AuthenticationException; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.endpoint.AuthorizationRequestAttributes; +import org.springframework.security.oauth2.core.endpoint.OAuth2Parameter; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.security.oauth2.client.authentication.TestUtil.*; + +/** + * Tests {@link AuthorizationCodeAuthenticationProcessingFilter}. + * + * @author Joe Grandja + */ +public class AuthorizationCodeAuthenticationProcessingFilterTests { + + @Test + public void doFilterWhenNotAuthorizationCodeResponseThenContinueChain() throws Exception { + ClientRegistration clientRegistration = googleClientRegistration(); + + AuthorizationCodeAuthenticationProcessingFilter filter = spy(setupFilter(clientRegistration)); + + String requestURI = "/path"; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestURI); + request.setServletPath(requestURI); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + filter.doFilter(request, response, filterChain); + + verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + verify(filter, never()).attemptAuthentication(any(HttpServletRequest.class), any(HttpServletResponse.class)); + } + + @Test + public void doFilterWhenAuthorizationCodeErrorResponseThenAuthenticationFailureHandlerIsCalled() throws Exception { + ClientRegistration clientRegistration = githubClientRegistration(); + + AuthorizationCodeAuthenticationProcessingFilter filter = spy(setupFilter(clientRegistration)); + AuthenticationFailureHandler failureHandler = mock(AuthenticationFailureHandler.class); + filter.setAuthenticationFailureHandler(failureHandler); + + MockHttpServletRequest request = this.setupRequest(clientRegistration); + String errorCode = OAuth2Error.INVALID_GRANT_ERROR_CODE; + request.addParameter(OAuth2Parameter.ERROR, errorCode); + request.addParameter(OAuth2Parameter.STATE, "some state"); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + filter.doFilter(request, response, filterChain); + + verify(filter).attemptAuthentication(any(HttpServletRequest.class), any(HttpServletResponse.class)); + verify(failureHandler).onAuthenticationFailure(any(HttpServletRequest.class), any(HttpServletResponse.class), + any(AuthenticationException.class)); + } + + @Test + public void doFilterWhenAuthorizationCodeSuccessResponseThenAuthenticationSuccessHandlerIsCalled() throws Exception { + TestingAuthenticationToken authentication = new TestingAuthenticationToken("joe", "password", "user", "admin"); + AuthenticationManager authenticationManager = mock(AuthenticationManager.class); + when(authenticationManager.authenticate(any(Authentication.class))).thenReturn(authentication); + + ClientRegistration clientRegistration = githubClientRegistration(); + + AuthorizationCodeAuthenticationProcessingFilter filter = spy(setupFilter(authenticationManager, clientRegistration)); + AuthenticationSuccessHandler successHandler = mock(AuthenticationSuccessHandler.class); + filter.setAuthenticationSuccessHandler(successHandler); + AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionAuthorizationRequestRepository(); + filter.setAuthorizationRequestRepository(authorizationRequestRepository); + + MockHttpServletRequest request = this.setupRequest(clientRegistration); + String authCode = "some code"; + String state = "some state"; + request.addParameter(OAuth2Parameter.CODE, authCode); + request.addParameter(OAuth2Parameter.STATE, state); + setupAuthorizationRequest(authorizationRequestRepository, request, clientRegistration, state); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + filter.doFilter(request, response, filterChain); + + verify(filter).attemptAuthentication(any(HttpServletRequest.class), any(HttpServletResponse.class)); + + ArgumentCaptor authenticationArgCaptor = ArgumentCaptor.forClass(Authentication.class); + verify(successHandler).onAuthenticationSuccess(any(HttpServletRequest.class), any(HttpServletResponse.class), + authenticationArgCaptor.capture()); + assertThat(authenticationArgCaptor.getValue()).isEqualTo(authentication); + } + + @Test + public void doFilterWhenAuthorizationCodeSuccessResponseAndNoMatchingAuthorizationRequestThenThrowOAuth2AuthenticationExceptionAuthorizationRequestNotFound() throws Exception { + ClientRegistration clientRegistration = githubClientRegistration(); + + AuthorizationCodeAuthenticationProcessingFilter filter = spy(setupFilter(clientRegistration)); + AuthenticationFailureHandler failureHandler = mock(AuthenticationFailureHandler.class); + filter.setAuthenticationFailureHandler(failureHandler); + + MockHttpServletRequest request = this.setupRequest(clientRegistration); + String authCode = "some code"; + String state = "some state"; + request.addParameter(OAuth2Parameter.CODE, authCode); + request.addParameter(OAuth2Parameter.STATE, state); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + filter.doFilter(request, response, filterChain); + + verifyThrowsOAuth2AuthenticationExceptionWithErrorCode(filter, failureHandler, "authorization_request_not_found"); + } + + @Test + public void doFilterWhenAuthorizationCodeSuccessResponseWithInvalidStateParamThenThrowOAuth2AuthenticationExceptionInvalidStateParameter() throws Exception { + ClientRegistration clientRegistration = githubClientRegistration(); + + AuthorizationCodeAuthenticationProcessingFilter filter = spy(setupFilter(clientRegistration)); + AuthenticationFailureHandler failureHandler = mock(AuthenticationFailureHandler.class); + filter.setAuthenticationFailureHandler(failureHandler); + AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionAuthorizationRequestRepository(); + filter.setAuthorizationRequestRepository(authorizationRequestRepository); + + MockHttpServletRequest request = this.setupRequest(clientRegistration); + String authCode = "some code"; + String state = "some other state"; + request.addParameter(OAuth2Parameter.CODE, authCode); + request.addParameter(OAuth2Parameter.STATE, state); + setupAuthorizationRequest(authorizationRequestRepository, request, clientRegistration, "some state"); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + filter.doFilter(request, response, filterChain); + + verifyThrowsOAuth2AuthenticationExceptionWithErrorCode(filter, failureHandler, "invalid_state_parameter"); + } + + @Test + public void doFilterWhenAuthorizationCodeSuccessResponseWithInvalidRedirectUriParamThenThrowOAuth2AuthenticationExceptionInvalidRedirectUriParameter() throws Exception { + ClientRegistration clientRegistration = githubClientRegistration(); + + AuthorizationCodeAuthenticationProcessingFilter filter = spy(setupFilter(clientRegistration)); + AuthenticationFailureHandler failureHandler = mock(AuthenticationFailureHandler.class); + filter.setAuthenticationFailureHandler(failureHandler); + AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionAuthorizationRequestRepository(); + filter.setAuthorizationRequestRepository(authorizationRequestRepository); + + MockHttpServletRequest request = this.setupRequest(clientRegistration); + request.setRequestURI(request.getRequestURI() + "-other"); + String authCode = "some code"; + String state = "some state"; + request.addParameter(OAuth2Parameter.CODE, authCode); + request.addParameter(OAuth2Parameter.STATE, state); + setupAuthorizationRequest(authorizationRequestRepository, request, clientRegistration, state); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + filter.doFilter(request, response, filterChain); + + verifyThrowsOAuth2AuthenticationExceptionWithErrorCode(filter, failureHandler, "invalid_redirect_uri_parameter"); + } + + private void verifyThrowsOAuth2AuthenticationExceptionWithErrorCode(AuthorizationCodeAuthenticationProcessingFilter filter, + AuthenticationFailureHandler failureHandler, + String errorCode) throws Exception { + + verify(filter).attemptAuthentication(any(HttpServletRequest.class), any(HttpServletResponse.class)); + + ArgumentCaptor authenticationExceptionArgCaptor = + ArgumentCaptor.forClass(AuthenticationException.class); + verify(failureHandler).onAuthenticationFailure(any(HttpServletRequest.class), any(HttpServletResponse.class), + authenticationExceptionArgCaptor.capture()); + assertThat(authenticationExceptionArgCaptor.getValue()).isInstanceOf(OAuth2AuthenticationException.class); + OAuth2AuthenticationException oauth2AuthenticationException = + (OAuth2AuthenticationException)authenticationExceptionArgCaptor.getValue(); + assertThat(oauth2AuthenticationException.getErrorObject()).isNotNull(); + assertThat(oauth2AuthenticationException.getErrorObject().getErrorCode()).isEqualTo(errorCode); + } + + private AuthorizationCodeAuthenticationProcessingFilter setupFilter(ClientRegistration... clientRegistrations) throws Exception { + AuthenticationManager authenticationManager = mock(AuthenticationManager.class); + + return setupFilter(authenticationManager, clientRegistrations); + } + + private AuthorizationCodeAuthenticationProcessingFilter setupFilter( + AuthenticationManager authenticationManager, ClientRegistration... clientRegistrations) throws Exception { + + ClientRegistrationRepository clientRegistrationRepository = clientRegistrationRepository(clientRegistrations); + + AuthorizationCodeAuthenticationProcessingFilter filter = new AuthorizationCodeAuthenticationProcessingFilter(); + filter.setClientRegistrationRepository(clientRegistrationRepository); + filter.setAuthenticationManager(authenticationManager); + + return filter; + } + + private void setupAuthorizationRequest(AuthorizationRequestRepository authorizationRequestRepository, + HttpServletRequest request, + ClientRegistration clientRegistration, + String state) { + + AuthorizationRequestAttributes authorizationRequestAttributes = + AuthorizationRequestAttributes.withAuthorizationCode() + .clientId(clientRegistration.getClientId()) + .authorizeUri(clientRegistration.getProviderDetails().getAuthorizationUri()) + .redirectUri(clientRegistration.getRedirectUri()) + .scopes(clientRegistration.getScopes()) + .state(state) + .build(); + + authorizationRequestRepository.saveAuthorizationRequest(authorizationRequestAttributes, request); + } + + private MockHttpServletRequest setupRequest(ClientRegistration clientRegistration) { + String requestURI = AUTHORIZE_BASE_URI + "/" + clientRegistration.getClientAlias(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestURI); + request.setScheme(DEFAULT_SCHEME); + request.setServerName(DEFAULT_SERVER_NAME); + request.setServerPort(DEFAULT_SERVER_PORT); + request.setServletPath(requestURI); + return request; + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeRequestRedirectFilterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeRequestRedirectFilterTests.java new file mode 100644 index 0000000000..cf68361cf3 --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeRequestRedirectFilterTests.java @@ -0,0 +1,142 @@ +/* + * Copyright 2012-2017 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.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.core.endpoint.AuthorizationRequestAttributes; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.net.URI; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; +import static org.springframework.security.oauth2.client.authentication.TestUtil.*; + +/** + * Tests {@link AuthorizationCodeRequestRedirectFilter}. + * + * @author Joe Grandja + */ +public class AuthorizationCodeRequestRedirectFilterTests { + + @Test(expected = IllegalArgumentException.class) + public void constructorWhenClientRegistrationRepositoryIsNullThenThrowIllegalArgumentException() { + new AuthorizationCodeRequestRedirectFilter(null, mock(AuthorizationRequestUriBuilder.class)); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorWhenAuthorizationRequestUriBuilderIsNullThenThrowIllegalArgumentException() { + new AuthorizationCodeRequestRedirectFilter(mock(ClientRegistrationRepository.class), null); + } + + @Test + public void doFilterWhenRequestDoesNotMatchClientThenContinueChain() throws Exception { + ClientRegistration clientRegistration = googleClientRegistration(); + String authorizationUri = clientRegistration.getProviderDetails().getAuthorizationUri().toString(); + AuthorizationCodeRequestRedirectFilter filter = + setupFilter(authorizationUri, clientRegistration); + + String requestURI = "/path"; + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestURI); + request.setServletPath(requestURI); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + filter.doFilter(request, response, filterChain); + + verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + } + + @Test + public void doFilterWhenRequestMatchesClientThenRedirectForAuthorization() throws Exception { + ClientRegistration clientRegistration = googleClientRegistration(); + String authorizationUri = clientRegistration.getProviderDetails().getAuthorizationUri().toString(); + AuthorizationCodeRequestRedirectFilter filter = + setupFilter(authorizationUri, clientRegistration); + + String requestUri = AUTHORIZATION_BASE_URI + "/" + clientRegistration.getClientAlias(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + filter.doFilter(request, response, filterChain); + + verifyZeroInteractions(filterChain); // Request should not proceed up the chain + + assertThat(response.getRedirectedUrl()).isEqualTo(authorizationUri); + } + + @Test + public void doFilterWhenRequestMatchesClientThenAuthorizationRequestSavedInSession() throws Exception { + ClientRegistration clientRegistration = githubClientRegistration(); + String authorizationUri = clientRegistration.getProviderDetails().getAuthorizationUri().toString(); + AuthorizationCodeRequestRedirectFilter filter = + setupFilter(authorizationUri, clientRegistration); + AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionAuthorizationRequestRepository(); + filter.setAuthorizationRequestRepository(authorizationRequestRepository); + + String requestUri = AUTHORIZATION_BASE_URI + "/" + clientRegistration.getClientAlias(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri); + request.setServletPath(requestUri); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + filter.doFilter(request, response, filterChain); + + verifyZeroInteractions(filterChain); // Request should not proceed up the chain + + // The authorization request attributes are saved in the session before the redirect happens + AuthorizationRequestAttributes authorizationRequestAttributes = + authorizationRequestRepository.loadAuthorizationRequest(request); + assertThat(authorizationRequestAttributes).isNotNull(); + + assertThat(authorizationRequestAttributes.getAuthorizeUri()).isNotNull(); + assertThat(authorizationRequestAttributes.getGrantType()).isNotNull(); + assertThat(authorizationRequestAttributes.getResponseType()).isNotNull(); + assertThat(authorizationRequestAttributes.getClientId()).isNotNull(); + assertThat(authorizationRequestAttributes.getRedirectUri()).isNotNull(); + assertThat(authorizationRequestAttributes.getScopes()).isNotNull(); + assertThat(authorizationRequestAttributes.getState()).isNotNull(); + } + + private AuthorizationCodeRequestRedirectFilter setupFilter(String authorizationUri, + ClientRegistration... clientRegistrations) throws Exception { + + AuthorizationRequestUriBuilder authorizationUriBuilder = mock(AuthorizationRequestUriBuilder.class); + URI authorizationURI = new URI(authorizationUri); + when(authorizationUriBuilder.build(any(AuthorizationRequestAttributes.class))).thenReturn(authorizationURI); + + return setupFilter(authorizationUriBuilder, clientRegistrations); + } + + private AuthorizationCodeRequestRedirectFilter setupFilter(AuthorizationRequestUriBuilder authorizationUriBuilder, + ClientRegistration... clientRegistrations) throws Exception { + + ClientRegistrationRepository clientRegistrationRepository = clientRegistrationRepository(clientRegistrations); + + AuthorizationCodeRequestRedirectFilter filter = new AuthorizationCodeRequestRedirectFilter( + clientRegistrationRepository, authorizationUriBuilder); + + return filter; + } +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/TestUtil.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/TestUtil.java new file mode 100644 index 0000000000..5d6fe081bb --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/TestUtil.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-2017 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.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationProperties; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; + +import java.util.Arrays; +import java.util.stream.Collectors; + +/** + * @author Joe Grandja + */ +class TestUtil { + static final String DEFAULT_SCHEME = "https"; + static final String DEFAULT_SERVER_NAME = "localhost"; + static final int DEFAULT_SERVER_PORT = 8080; + static final String DEFAULT_SERVER_URL = DEFAULT_SCHEME + "://" + DEFAULT_SERVER_NAME + ":" + DEFAULT_SERVER_PORT; + static final String AUTHORIZATION_BASE_URI = "/oauth2/authorization/code"; + static final String AUTHORIZE_BASE_URI = "/oauth2/authorize/code"; + static final String GOOGLE_CLIENT_ALIAS = "google"; + static final String GITHUB_CLIENT_ALIAS = "github"; + + static ClientRegistrationRepository clientRegistrationRepository(ClientRegistration... clientRegistrations) { + return new InMemoryClientRegistrationRepository(Arrays.asList(clientRegistrations)); + } + + static ClientRegistration googleClientRegistration() { + return googleClientRegistration(DEFAULT_SERVER_URL + AUTHORIZE_BASE_URI + "/" + GOOGLE_CLIENT_ALIAS); + } + + static ClientRegistration googleClientRegistration(String redirectUri) { + ClientRegistrationProperties clientRegistrationProperties = new ClientRegistrationProperties(); + clientRegistrationProperties.setClientId("google-client-id"); + clientRegistrationProperties.setClientSecret("secret"); + clientRegistrationProperties.setAuthorizedGrantType(AuthorizationGrantType.AUTHORIZATION_CODE); + clientRegistrationProperties.setClientName("Google Client"); + clientRegistrationProperties.setClientAlias(GOOGLE_CLIENT_ALIAS); + clientRegistrationProperties.setAuthorizationUri("https://accounts.google.com/o/oauth2/auth"); + clientRegistrationProperties.setTokenUri("https://accounts.google.com/o/oauth2/token"); + clientRegistrationProperties.setUserInfoUri("https://www.googleapis.com/oauth2/v3/userinfo"); + clientRegistrationProperties.setRedirectUri(redirectUri); + clientRegistrationProperties.setScopes(Arrays.stream(new String[] {"openid", "email", "profile"}).collect(Collectors.toSet())); + return new ClientRegistration.Builder(clientRegistrationProperties).build(); + } + + static ClientRegistration githubClientRegistration() { + return githubClientRegistration(DEFAULT_SERVER_URL + AUTHORIZE_BASE_URI + "/" + GITHUB_CLIENT_ALIAS); + } + + static ClientRegistration githubClientRegistration(String redirectUri) { + ClientRegistrationProperties clientRegistrationProperties = new ClientRegistrationProperties(); + clientRegistrationProperties.setClientId("github-client-id"); + clientRegistrationProperties.setClientSecret("secret"); + clientRegistrationProperties.setAuthorizedGrantType(AuthorizationGrantType.AUTHORIZATION_CODE); + clientRegistrationProperties.setClientName("GitHub Client"); + clientRegistrationProperties.setClientAlias(GITHUB_CLIENT_ALIAS); + clientRegistrationProperties.setAuthorizationUri("https://github.com/login/oauth/authorize"); + clientRegistrationProperties.setTokenUri("https://github.com/login/oauth/access_token"); + clientRegistrationProperties.setUserInfoUri("https://api.github.com/user"); + clientRegistrationProperties.setRedirectUri(redirectUri); + clientRegistrationProperties.setScopes(Arrays.stream(new String[] {"user"}).collect(Collectors.toSet())); + return new ClientRegistration.Builder(clientRegistrationProperties).build(); + } +} diff --git a/oauth2/oauth2-core/pom.xml b/oauth2/oauth2-core/pom.xml new file mode 100644 index 0000000000..bf82370f24 --- /dev/null +++ b/oauth2/oauth2-core/pom.xml @@ -0,0 +1,138 @@ + + + 4.0.0 + org.springframework.security + spring-security-oauth2-core + 5.0.0.BUILD-SNAPSHOT + spring-security-oauth2-core + spring-security-oauth2-core + http://spring.io/spring-security + + spring.io + http://spring.io/ + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + rwinch + Rob Winch + rwinch@pivotal.io + + + jgrandja + Joe Grandja + jgrandja@pivotal.io + + + + scm:git:git://github.com/spring-projects/spring-security + scm:git:git://github.com/spring-projects/spring-security + https://github.com/spring-projects/spring-security + + + + + org.springframework + spring-framework-bom + 4.3.5.RELEASE + pom + import + + + + + + org.springframework.security + spring-security-core + 5.0.0.BUILD-SNAPSHOT + compile + + + org.springframework + spring-core + compile + + + commons-logging + commons-logging + + + + + org.springframework + spring-web + compile + + + commons-logging + commons-logging + 1.2 + compile + true + + + javax.servlet + javax.servlet-api + 3.1.0 + provided + + + ch.qos.logback + logback-classic + 1.1.2 + test + + + junit + junit + 4.12 + test + + + org.assertj + assertj-core + 3.6.2 + test + + + org.mockito + mockito-core + 1.10.19 + test + + + org.slf4j + jcl-over-slf4j + 1.7.7 + test + + + org.springframework + spring-test + test + + + + + spring-snapshot + https://repo.spring.io/snapshot + + + + + + maven-compiler-plugin + + 1.8 + 1.8 + + + + + diff --git a/oauth2/oauth2-core/spring-security-oauth2-core.gradle b/oauth2/oauth2-core/spring-security-oauth2-core.gradle new file mode 100644 index 0000000000..83ea464e63 --- /dev/null +++ b/oauth2/oauth2-core/spring-security-oauth2-core.gradle @@ -0,0 +1,9 @@ +apply plugin: 'io.spring.convention.spring-module' + +dependencies { + compile project(':spring-security-core') + compile springCoreDependency + compile 'org.springframework:spring-web' + + provided 'javax.servlet:javax.servlet-api' +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AbstractToken.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AbstractToken.java new file mode 100644 index 0000000000..39fca83d6e --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AbstractToken.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2017 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.core; + +import org.springframework.security.core.SpringSecurityCoreVersion; +import org.springframework.util.Assert; + +import java.io.Serializable; +import java.time.Instant; + +/** + * Base class for Security Token implementations. + * + *

+ * It is highly recommended that implementations be immutable. + * + * @author Joe Grandja + * @since 5.0 + */ +public abstract class AbstractToken implements Serializable { + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + private final String tokenValue; + private final Instant issuedAt; + private final Instant expiresAt; + + protected AbstractToken(String tokenValue, Instant issuedAt, Instant expiresAt) { + Assert.hasLength(tokenValue, "tokenValue cannot be empty"); + Assert.notNull(issuedAt, "issuedAt cannot be null"); + Assert.notNull(expiresAt, "expiresAt cannot be null"); + this.tokenValue = tokenValue; + this.issuedAt = issuedAt; + this.expiresAt = expiresAt; + } + + public String getTokenValue() { + return this.tokenValue; + } + + public Instant getIssuedAt() { + return this.issuedAt; + } + + public Instant getExpiresAt() { + return this.expiresAt; + } +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AccessToken.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AccessToken.java new file mode 100644 index 0000000000..097c3a0c22 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AccessToken.java @@ -0,0 +1,88 @@ +/* + * Copyright 2012-2017 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.core; + +import org.springframework.util.Assert; + +import java.time.Instant; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +/** + * An implementation of an {@link AbstractToken} representing an OAuth 2.0 Access Token. + * + *

+ * An access token is a credential that represents an authorization + * granted by the resource owner to the client. + * It is primarily used by the client to access protected resources on either a + * resource server or the authorization server that originally issued the access token. + * + * @author Joe Grandja + * @since 5.0 + * @see Section 1.4 Access Token + */ +public class AccessToken extends AbstractToken { + private final TokenType tokenType; + private final Set scopes; + private final Map additionalParameters; + + public enum TokenType { + BEARER("Bearer"); + + private final String value; + + TokenType(String value) { + this.value = value; + } + + public String value() { + return this.value; + } + } + + public AccessToken(TokenType tokenType, String tokenValue, Instant issuedAt, Instant expiresAt) { + this(tokenType, tokenValue, issuedAt, expiresAt, Collections.emptySet()); + } + + public AccessToken(TokenType tokenType, String tokenValue, Instant issuedAt, Instant expiresAt, Set scopes) { + this(tokenType, tokenValue, issuedAt, expiresAt, scopes, Collections.emptyMap()); + } + + public AccessToken(TokenType tokenType, String tokenValue, Instant issuedAt, Instant expiresAt, + Set scopes, Map additionalParameters) { + + super(tokenValue, issuedAt, expiresAt); + Assert.notNull(tokenType, "tokenType cannot be null"); + this.tokenType = tokenType; + this.scopes = Collections.unmodifiableSet( + scopes != null ? scopes : Collections.emptySet()); + this.additionalParameters = Collections.unmodifiableMap( + additionalParameters != null ? additionalParameters : Collections.emptyMap()); + } + + public TokenType getTokenType() { + return this.tokenType; + } + + public Set getScopes() { + return this.scopes; + } + + public Map getAdditionalParameters() { + return additionalParameters; + } +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java new file mode 100644 index 0000000000..dc36331720 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2017 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.core; + +/** + * An authorization grant is a credential representing the resource owner's authorization + * (to access it's protected resources) to the client and used by the client to obtain an access token. + * + *

+ * The OAuth 2.0 Authorization Framework defines four standard grant types: + * authorization code, implicit, resource owner password credentials, and client credentials. + * It also provides an extensibility mechanism for defining additional grant types. + * + *

+ * NOTE: "authorization code" is currently the only supported grant type. + * + * @author Joe Grandja + * @since 5.0 + * @see Section 1.3 Authorization Grant + */ +public enum AuthorizationGrantType { + AUTHORIZATION_CODE("authorization_code"); + + private final String value; + + AuthorizationGrantType(String value) { + this.value = value; + } + + public String value() { + return this.value; + } +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClientAuthenticationMethod.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClientAuthenticationMethod.java new file mode 100644 index 0000000000..19a3bf1031 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClientAuthenticationMethod.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2017 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.core; + +/** + * The available authentication methods used when authenticating the client with the authorization server. + * + * @author Joe Grandja + * @since 5.0 + * @see Section 2.3 Client Authentication + */ +public enum ClientAuthenticationMethod { + HEADER("header"), + FORM("form"); + + private final String value; + + ClientAuthenticationMethod(String value) { + this.value = value; + } + + public String value() { + return this.value; + } +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2Error.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2Error.java new file mode 100644 index 0000000000..39841a9fcf --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2Error.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2017 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.core; + +import org.springframework.util.Assert; + +/** + * A representation of an OAuth 2.0 Error. + * + *

+ * At a minimum, an error response will contain an error code. + * The error code may be one of the standard codes defined by the specification, + * or a new code defined in the OAuth Extensions Error Registry, + * for cases where protocol extensions require additional error code(s) above the standard codes. + * + * @author Joe Grandja + * @since 5.0 + * @see Section 11.4 OAuth Extensions Error Registry + */ +public class OAuth2Error { + // Standard error codes + public static final String INVALID_REQUEST_ERROR_CODE = "invalid_request"; + public static final String INVALID_CLIENT_ERROR_CODE = "invalid_client"; + public static final String INVALID_GRANT_ERROR_CODE = "invalid_grant"; + public static final String UNAUTHORIZED_CLIENT_ERROR_CODE = "unauthorized_client"; + public static final String UNSUPPORTED_GRANT_TYPE_ERROR_CODE = "unsupported_grant_type"; + public static final String INVALID_SCOPE_ERROR_CODE = "invalid_scope"; + + private final String errorCode; + private final String description; + private final String uri; + + public OAuth2Error(String errorCode) { + this(errorCode, null, null); + } + + public OAuth2Error(String errorCode, String description, String uri) { + Assert.hasText(errorCode, "errorCode cannot be empty"); + this.errorCode = errorCode; + this.description = description; + this.uri = uri; + } + + public String getErrorCode() { + return this.errorCode; + } + + public String getDescription() { + return this.description; + } + + public String getUri() { + return this.uri; + } + + @Override + public String toString() { + return "[" + this.getErrorCode() + "] " + + (this.getDescription() != null ? this.getDescription() : ""); + } +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/AuthorizationCodeAuthorizationResponseAttributes.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/AuthorizationCodeAuthorizationResponseAttributes.java new file mode 100644 index 0000000000..707fd78c9d --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/AuthorizationCodeAuthorizationResponseAttributes.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2017 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.core.endpoint; + +import org.springframework.util.Assert; + +/** + * A representation of an OAuth 2.0 Authorization Response for the authorization code grant type. + * + * @author Joe Grandja + * @since 5.0 + * @see Section 4.1.2 Authorization Response + */ +public final class AuthorizationCodeAuthorizationResponseAttributes { + private final String code; + private final String state; + + public AuthorizationCodeAuthorizationResponseAttributes(String code, String state) { + Assert.notNull(code, "code cannot be null"); + this.code = code; + this.state = state; + } + + public String getCode() { + return this.code; + } + + public String getState() { + return this.state; + } +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/AuthorizationCodeTokenRequestAttributes.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/AuthorizationCodeTokenRequestAttributes.java new file mode 100644 index 0000000000..ce8443baaf --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/AuthorizationCodeTokenRequestAttributes.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-2017 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.core.endpoint; + +import org.springframework.util.Assert; + +/** + * A representation of an OAuth 2.0 Access Token Request for the authorization code grant type. + * + * @author Joe Grandja + * @since 5.0 + * @see Section 4.1.3 Access Token Request + */ +public final class AuthorizationCodeTokenRequestAttributes { + private String code; + private String clientId; + private String redirectUri; + + private AuthorizationCodeTokenRequestAttributes() { + } + + public String getCode() { + return this.code; + } + + public String getClientId() { + return this.clientId; + } + + public String getRedirectUri() { + return this.redirectUri; + } + + public static Builder withCode(String code) { + return new Builder(code); + } + + public static class Builder { + private final AuthorizationCodeTokenRequestAttributes authorizationCodeTokenRequest; + + private Builder(String code) { + Assert.hasText(code, "code cannot be empty"); + this.authorizationCodeTokenRequest = new AuthorizationCodeTokenRequestAttributes(); + this.authorizationCodeTokenRequest.code = code; + } + + public Builder clientId(String clientId) { + Assert.hasText(clientId, "clientId cannot be empty"); + this.authorizationCodeTokenRequest.clientId = clientId; + return this; + } + + public Builder redirectUri(String redirectUri) { + Assert.hasText(redirectUri, "redirectUri cannot be empty"); + this.authorizationCodeTokenRequest.redirectUri = redirectUri; + return this; + } + + public AuthorizationCodeTokenRequestAttributes build() { + return this.authorizationCodeTokenRequest; + } + } +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/AuthorizationRequestAttributes.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/AuthorizationRequestAttributes.java new file mode 100644 index 0000000000..a4ea4c0e3d --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/AuthorizationRequestAttributes.java @@ -0,0 +1,127 @@ +/* + * Copyright 2012-2017 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.core.endpoint; + +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +import java.io.Serializable; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * A representation of an OAuth 2.0 Authorization Request + * for the authorization code grant type or implicit grant type. + * + * @author Joe Grandja + * @since 5.0 + * @see AuthorizationGrantType + * @see ResponseType + * @see Section 4.1.1 Authorization Code Grant Request + * @see Section 4.2.1 Implicit Grant Request + */ +public final class AuthorizationRequestAttributes implements Serializable { + private String authorizeUri; + private AuthorizationGrantType authorizationGrantType; + private ResponseType responseType; + private String clientId; + private String redirectUri; + private Set scopes; + private String state; + + private AuthorizationRequestAttributes() { + } + + public String getAuthorizeUri() { + return this.authorizeUri; + } + + public AuthorizationGrantType getGrantType() { + return this.authorizationGrantType; + } + + public ResponseType getResponseType() { + return this.responseType; + } + + public String getClientId() { + return this.clientId; + } + + public String getRedirectUri() { + return this.redirectUri; + } + + public Set getScopes() { + return this.scopes; + } + + public String getState() { + return this.state; + } + + public static Builder withAuthorizationCode() { + return new Builder(AuthorizationGrantType.AUTHORIZATION_CODE); + } + + public static class Builder { + private final AuthorizationRequestAttributes authorizationRequest; + + private Builder(AuthorizationGrantType authorizationGrantType) { + Assert.notNull(authorizationGrantType, "authorizationGrantType cannot be null"); + this.authorizationRequest = new AuthorizationRequestAttributes(); + this.authorizationRequest.authorizationGrantType = authorizationGrantType; + if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationGrantType)) { + this.authorizationRequest.responseType = ResponseType.CODE; + } + } + + public Builder authorizeUri(String authorizeUri) { + Assert.hasText(authorizeUri, "authorizeUri cannot be empty"); + this.authorizationRequest.authorizeUri = authorizeUri; + return this; + } + + public Builder clientId(String clientId) { + Assert.hasText(clientId, "clientId cannot be empty"); + this.authorizationRequest.clientId = clientId; + return this; + } + + public Builder redirectUri(String redirectUri) { + Assert.hasText(redirectUri, "redirectUri cannot be empty"); + this.authorizationRequest.redirectUri = redirectUri; + return this; + } + + public Builder scopes(Set scopes) { + this.authorizationRequest.scopes = Collections.unmodifiableSet( + CollectionUtils.isEmpty(scopes) ? Collections.emptySet() : new LinkedHashSet<>(scopes)); + return this; + } + + public Builder state(String state) { + this.authorizationRequest.state = state; + return this; + } + + public AuthorizationRequestAttributes build() { + return this.authorizationRequest; + } + } +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/ErrorResponseAttributes.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/ErrorResponseAttributes.java new file mode 100644 index 0000000000..dca297df36 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/ErrorResponseAttributes.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-2017 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.core.endpoint; + +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.util.Assert; + +/** + * A representation of an OAuth 2.0 Error Response. + * + *

+ * An error response may be returned from either of the following locations: + *

+ * + * @author Joe Grandja + * @since 5.0 + */ +public final class ErrorResponseAttributes { + private OAuth2Error errorObject; + private String state; + + private ErrorResponseAttributes() { + } + + public String getErrorCode() { + return this.errorObject.getErrorCode(); + } + + public String getDescription() { + return this.errorObject.getDescription(); + } + + public String getUri() { + return this.errorObject.getUri(); + } + + public String getState() { + return this.state; + } + + public static Builder withErrorCode(String errorCode) { + return new Builder(errorCode); + } + + public static class Builder { + private String errorCode; + private String description; + private String uri; + private String state; + + private Builder(String errorCode) { + Assert.hasText(errorCode, "errorCode cannot be empty"); + this.errorCode = errorCode; + } + + public Builder description(String description) { + this.description = description; + return this; + } + + public Builder uri(String uri) { + this.uri = uri; + return this; + } + + public Builder state(String state) { + this.state = state; + return this; + } + + public ErrorResponseAttributes build() { + ErrorResponseAttributes errorResponse = new ErrorResponseAttributes(); + errorResponse.errorObject = new OAuth2Error(this.errorCode, this.description, this.uri); + errorResponse.state = this.state; + return errorResponse; + } + } +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2Parameter.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2Parameter.java new file mode 100644 index 0000000000..fd3becc823 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2Parameter.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2017 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.core.endpoint; + +/** + * Standard parameters defined in the OAuth Parameters Registry + * and used by the authorization endpoint and token endpoint. + * + * @author Joe Grandja + * @since 5.0 + * @see 11.2 OAuth Parameters Registry + */ +public interface OAuth2Parameter { + + String RESPONSE_TYPE = "response_type"; + + String CLIENT_ID = "client_id"; + + String REDIRECT_URI = "redirect_uri"; + + String SCOPE = "scope"; + + String STATE = "state"; + + String CODE = "code"; + + String ERROR = "error"; + + String ERROR_DESCRIPTION = "error_description"; + + String ERROR_URI = "error_uri"; + +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/ResponseType.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/ResponseType.java new file mode 100644 index 0000000000..c443682442 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/ResponseType.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2017 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.core.endpoint; + +/** + * The response_type parameter is consumed by the authorization endpoint which + * is used by the authorization code grant type and implicit grant type flows. + * The client sets the response_type parameter with the desired grant type before initiating the authorization request. + * + *

+ * The response_type parameter value may be one of "code" for requesting an authorization code or + * "token" for requesting an access token (implicit grant). + + *

+ * NOTE: "code" is currently the only supported response type. + * + * @author Joe Grandja + * @since 5.0 + * @see Section 3.1.1 Response Type + */ +public enum ResponseType { + CODE("code"); + + private final String value; + + ResponseType(String value) { + this.value = value; + } + + public String value() { + return this.value; + } +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/TokenResponseAttributes.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/TokenResponseAttributes.java new file mode 100644 index 0000000000..0743aafdfd --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/TokenResponseAttributes.java @@ -0,0 +1,109 @@ +/* + * Copyright 2012-2017 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.core.endpoint; + +import org.springframework.security.oauth2.core.AccessToken; +import org.springframework.util.Assert; + +import java.time.Instant; +import java.util.Map; +import java.util.Set; + +/** + * A representation of an OAuth 2.0 Access Token Response. + * + * @author Joe Grandja + * @since 5.0 + * @see AccessToken + * @see Section 5.1 Access Token Response + */ +public final class TokenResponseAttributes { + private AccessToken accessToken; + + private TokenResponseAttributes() { + } + + public String getTokenValue() { + return this.accessToken.getTokenValue(); + } + + public AccessToken.TokenType getTokenType() { + return this.accessToken.getTokenType(); + } + + public Instant getIssuedAt() { + return this.accessToken.getIssuedAt(); + } + + public Instant getExpiresAt() { + return this.accessToken.getExpiresAt(); + } + + public Set getScopes() { + return this.accessToken.getScopes(); + } + + public Map getAdditionalParameters() { + return this.accessToken.getAdditionalParameters(); + } + + public static Builder withToken(String tokenValue) { + return new Builder(tokenValue); + } + + public static class Builder { + private String tokenValue; + private AccessToken.TokenType tokenType; + private long expiresIn; + private Set scopes; + private Map additionalParameters; + + private Builder(String tokenValue) { + this.tokenValue = tokenValue; + } + + public Builder tokenType(AccessToken.TokenType tokenType) { + this.tokenType = tokenType; + return this; + } + + public Builder expiresIn(long expiresIn) { + this.expiresIn = expiresIn; + return this; + } + + public Builder scopes(Set scopes) { + this.scopes = scopes; + return this; + } + + public Builder additionalParameters(Map additionalParameters) { + this.additionalParameters = additionalParameters; + return this; + } + + public TokenResponseAttributes build() { + Assert.isTrue(this.expiresIn >= 0, "expiresIn must be a positive number"); + Instant issuedAt = Instant.now(); + AccessToken accessToken = new AccessToken(this.tokenType, this.tokenValue, issuedAt, + issuedAt.plusSeconds(this.expiresIn), this.scopes, this.additionalParameters); + + TokenResponseAttributes tokenResponse = new TokenResponseAttributes(); + tokenResponse.accessToken = accessToken; + return tokenResponse; + } + } +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/package-info.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/package-info.java new file mode 100644 index 0000000000..b529499a2f --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2017 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. + */ +/** + * Support classes that model the request/response messages from the + * Authorization Endpoint + * and Token Endpoint. + */ +package org.springframework.security.oauth2.core.endpoint; diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/package-info.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/package-info.java new file mode 100644 index 0000000000..01fd764fbe --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2012-2017 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. + */ +/** + * Core classes and interfaces providing support for the OAuth 2.0 Authorization Framework. + */ +package org.springframework.security.oauth2.core; diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/DefaultOAuth2User.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/DefaultOAuth2User.java new file mode 100644 index 0000000000..9139dd9b27 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/DefaultOAuth2User.java @@ -0,0 +1,153 @@ +/* + * Copyright 2012-2017 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.core.user; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.SpringSecurityCoreVersion; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +import java.time.Instant; +import java.util.*; +import java.util.stream.Collectors; + +/** + * The default implementation of an {@link OAuth2User}. + * + *

+ * User attribute names are not standardized between providers + * and therefore it is required that the user supply the key + * for the user's "name" attribute to one of the constructors. + * The key will be used for accessing the "name" of the + * Principal (user) via {@link #getAttributes()} + * and returning it from {@link #getName()}. + * + * @author Joe Grandja + * @since 5.0 + * @see OAuth2User + */ +public class DefaultOAuth2User implements OAuth2User { + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + private final Set authorities; + private final Map attributes; + private final String nameAttributeKey; + + public DefaultOAuth2User(Map attributes, String nameAttributeKey) { + this(Collections.emptySet(), attributes, nameAttributeKey); + } + + public DefaultOAuth2User(Set authorities, Map attributes, String nameAttributeKey) { + Assert.notNull(authorities, "authorities cannot be null"); + Assert.notEmpty(attributes, "attributes cannot be empty"); + Assert.hasText(nameAttributeKey, "nameAttributeKey cannot be empty"); + if (!attributes.containsKey(nameAttributeKey)) { + throw new IllegalArgumentException("Invalid nameAttributeKey: " + nameAttributeKey); + } + this.authorities = Collections.unmodifiableSet(this.sortAuthorities(authorities)); + this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(attributes)); + this.nameAttributeKey = nameAttributeKey; + } + + @Override + public String getName() { + return this.getAttributes().get(this.nameAttributeKey).toString(); + } + + @Override + public Collection getAuthorities() { + return this.authorities; + } + + @Override + public Map getAttributes() { + return this.attributes; + } + + protected String getAttributeAsString(String key) { + Object value = this.getAttributes().get(key); + return (value != null ? value.toString() : null); + } + + protected Boolean getAttributeAsBoolean(String key) { + String value = this.getAttributeAsString(key); + return (value != null ? Boolean.valueOf(value) : null); + } + + protected Instant getAttributeAsInstant(String key) { + String value = this.getAttributeAsString(key); + if (value == null) { + return null; + } + try { + return Instant.ofEpochSecond(Long.valueOf(value)); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException("Invalid long value: " + ex.getMessage(), ex); + } + } + + private Set sortAuthorities(Set authorities) { + if (CollectionUtils.isEmpty(authorities)) { + return Collections.emptySet(); + } + + SortedSet sortedAuthorities = + new TreeSet<>((g1, g2) -> g1.getAuthority().compareTo(g2.getAuthority())); + authorities.stream().forEach(sortedAuthorities::add); + + return sortedAuthorities; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + + DefaultOAuth2User that = (DefaultOAuth2User) obj; + + if (!this.getName().equals(that.getName())) { + return false; + } + if (!this.getAuthorities().equals(that.getAuthorities())) { + return false; + } + return this.getAttributes().equals(that.getAttributes()); + } + + @Override + public int hashCode() { + int result = this.getName().hashCode(); + result = 31 * result + this.getAuthorities().hashCode(); + result = 31 * result + this.getAttributes().hashCode(); + return result; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Name: ["); + sb.append(this.getName()); + sb.append("], Granted Authorities: ["); + sb.append(this.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining(", "))); + sb.append("], User Attributes: ["); + sb.append(this.getAttributes().entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining(", "))); + sb.append("]"); + return sb.toString(); + } +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/OAuth2User.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/OAuth2User.java new file mode 100644 index 0000000000..769cd3ba8b --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/OAuth2User.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2017 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.core.user; + +import org.springframework.security.core.AuthenticatedPrincipal; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Map; + +/** + * A representation of a user Principal + * that is registered with a standard OAuth 2.0 Provider. + * + *

+ * An OAuth 2.0 user is composed of one or more attributes, for example, + * first name, middle name, last name, email, phone number, address, etc. + * Each user attribute has a "name" and "value" and + * is keyed by the "name" in {@link #getAttributes()}. + * + *

+ * NOTE: Attribute names are not standardized between providers and therefore will vary. + * Please consult the provider's API documentation for the set of supported user attribute names. + * + *

+ * Implementation instances of this interface represent an {@link AuthenticatedPrincipal} + * which is associated to an {@link Authentication} object + * and may be accessed via {@link Authentication#getPrincipal()}. + * + * @author Joe Grandja + * @since 5.0 + * @see DefaultOAuth2User + * @see AuthenticatedPrincipal + */ +public interface OAuth2User extends AuthenticatedPrincipal, Serializable { + + Collection getAuthorities(); + + Map getAttributes(); +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/package-info.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/package-info.java new file mode 100644 index 0000000000..5223cbf768 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2012-2017 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. + */ +/** + * Provides a model for an OAuth 2.0 representation of a user Principal. + */ +package org.springframework.security.oauth2.core.user; diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/StandardClaimName.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/StandardClaimName.java new file mode 100644 index 0000000000..858174e54b --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/StandardClaimName.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2017 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.oidc; + +/** + * The Standard Claims defined by the OpenID Connect Core 1.0 specification + * and returned in either the UserInfo Response or in the ID Token. + * + * @author Joe Grandja + * @since 5.0 + * @see Standard Claims + * @see UserInfo Response + * @see ID Token + */ +public interface StandardClaimName { + + String SUB = "sub"; + + String NAME = "name"; + + String GIVEN_NAME = "given_name"; + + String FAMILY_NAME = "family_name"; + + String MIDDLE_NAME = "middle_name"; + + String NICKNAME = "nickname"; + + String PREFERRED_USERNAME = "preferred_username"; + + String PROFILE = "profile"; + + String PICTURE = "picture"; + + String WEBSITE = "website"; + + String EMAIL = "email"; + + String EMAIL_VERIFIED = "email_verified"; + + String GENDER = "gender"; + + String BIRTHDATE = "birthdate"; + + String ZONEINFO = "zoneinfo"; + + String LOCALE = "locale"; + + String PHONE_NUMBER = "phone_number"; + + String PHONE_NUMBER_VERIFIED = "phone_number_verified"; + + String ADDRESS = "address"; + + String UPDATED_AT = "updated_at"; +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/package-info.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/package-info.java new file mode 100644 index 0000000000..3aad8e1a64 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2012-2017 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. + */ +/** + * Core classes and interfaces providing support for OpenID Connect Core 1.0. + */ +package org.springframework.security.oauth2.oidc; diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/DefaultUserInfo.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/DefaultUserInfo.java new file mode 100644 index 0000000000..8d2f1f6ac7 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/DefaultUserInfo.java @@ -0,0 +1,154 @@ +/* + * Copyright 2012-2017 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.oidc.user; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.oidc.StandardClaimName; + +import java.time.Instant; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import static org.springframework.security.oauth2.oidc.StandardClaimName.*; + +/** + * The default implementation of a {@link UserInfo}. + * + *

+ * The key used for accessing the "name" of the + * Principal (user) via {@link #getAttributes()} + * is {@link StandardClaimName#NAME} or if not available + * will default to {@link StandardClaimName#SUB}. + * + * @author Joe Grandja + * @since 5.0 + * @see UserInfo + * @see DefaultOAuth2User + */ +public class DefaultUserInfo extends DefaultOAuth2User implements UserInfo { + + public DefaultUserInfo(Map attributes) { + this(Collections.emptySet(), attributes); + } + + public DefaultUserInfo(Set authorities, Map attributes) { + super(authorities, attributes, SUB); + } + + @Override + public String getSubject() { + return this.getAttributeAsString(SUB); + } + + @Override + public String getName() { + String name = this.getAttributeAsString(NAME); + return (name != null ? name : super.getName()); + } + + @Override + public String getGivenName() { + return this.getAttributeAsString(GIVEN_NAME); + } + + @Override + public String getFamilyName() { + return this.getAttributeAsString(FAMILY_NAME); + } + + @Override + public String getMiddleName() { + return this.getAttributeAsString(MIDDLE_NAME); + } + + @Override + public String getNickName() { + return this.getAttributeAsString(NICKNAME); + } + + @Override + public String getPreferredUsername() { + return this.getAttributeAsString(PREFERRED_USERNAME); + } + + @Override + public String getProfile() { + return this.getAttributeAsString(PROFILE); + } + + @Override + public String getPicture() { + return this.getAttributeAsString(PICTURE); + } + + @Override + public String getWebsite() { + return this.getAttributeAsString(WEBSITE); + } + + @Override + public String getEmail() { + return this.getAttributeAsString(EMAIL); + } + + @Override + public Boolean getEmailVerified() { + return this.getAttributeAsBoolean(EMAIL_VERIFIED); + } + + @Override + public String getGender() { + return this.getAttributeAsString(GENDER); + } + + @Override + public String getBirthdate() { + return this.getAttributeAsString(BIRTHDATE); + } + + @Override + public String getZoneInfo() { + return this.getAttributeAsString(ZONEINFO); + } + + @Override + public String getLocale() { + return this.getAttributeAsString(LOCALE); + } + + @Override + public String getPhoneNumber() { + return this.getAttributeAsString(PHONE_NUMBER); + } + + @Override + public Boolean getPhoneNumberVerified() { + return this.getAttributeAsBoolean(PHONE_NUMBER_VERIFIED); + } + + @Override + public Address getAddress() { + // TODO Impl + return null; + } + + @Override + public Instant getUpdatedAt() { + return this.getAttributeAsInstant(UPDATED_AT); + } +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/UserInfo.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/UserInfo.java new file mode 100644 index 0000000000..cd5861c012 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/UserInfo.java @@ -0,0 +1,103 @@ +/* + * Copyright 2012-2017 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.oidc.user; + +import org.springframework.security.core.AuthenticatedPrincipal; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.time.Instant; + +/** + * A representation of a user Principal + * that is registered with an OpenID Connect 1.0 Provider. + * + *

+ * The structure of the user Principal is defined by the + * UserInfo Endpoint, + * which is an OAuth 2.0 Protected Resource that returns a set of + * Claims + * about the authenticated End-User. + * + *

+ * Implementation instances of this interface represent an {@link AuthenticatedPrincipal} + * which is associated to an {@link Authentication} object + * and may be accessed via {@link Authentication#getPrincipal()}. + * + * @author Joe Grandja + * @since 5.0 + * @see DefaultUserInfo + * @see AuthenticatedPrincipal + * @see OpenID Connect Core 1.0 + * @see UserInfo Endpoint + * @see Standard Claims + */ +public interface UserInfo extends OAuth2User { + + String getSubject(); + + String getGivenName(); + + String getFamilyName(); + + String getMiddleName(); + + String getNickName(); + + String getPreferredUsername(); + + String getProfile(); + + String getPicture(); + + String getWebsite(); + + String getEmail(); + + Boolean getEmailVerified(); + + String getGender(); + + String getBirthdate(); + + String getZoneInfo(); + + String getLocale(); + + String getPhoneNumber(); + + Boolean getPhoneNumberVerified(); + + Address getAddress(); + + Instant getUpdatedAt(); + + + interface Address { + + String getFormatted(); + + String getStreetAddress(); + + String getLocality(); + + String getRegion(); + + String getPostalCode(); + + String getCountry(); + } +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/package-info.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/package-info.java new file mode 100644 index 0000000000..357fb0e8fa --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2012-2017 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. + */ +/** + * Provides a model for an OpenID Connect Core 1.0 representation of a user Principal. + */ +package org.springframework.security.oauth2.oidc.user; diff --git a/samples/boot/helloworld/pom.xml b/samples/boot/helloworld/pom.xml index 2575ea08b2..c86924e55b 100644 --- a/samples/boot/helloworld/pom.xml +++ b/samples/boot/helloworld/pom.xml @@ -44,6 +44,13 @@ pom import + + org.springframework.boot + spring-boot-dependencies + 1.5.0.BUILD-SNAPSHOT + pom + import + diff --git a/samples/boot/insecure/pom.xml b/samples/boot/insecure/pom.xml index 8d3149705a..08a85db947 100644 --- a/samples/boot/insecure/pom.xml +++ b/samples/boot/insecure/pom.xml @@ -44,6 +44,13 @@ pom import + + org.springframework.boot + spring-boot-dependencies + 1.5.0.BUILD-SNAPSHOT + pom + import + diff --git a/samples/boot/oauth2login/README.adoc b/samples/boot/oauth2login/README.adoc new file mode 100644 index 0000000000..d1e1224bf2 --- /dev/null +++ b/samples/boot/oauth2login/README.adoc @@ -0,0 +1,342 @@ += OAuth 2.0 Login Sample +Joe Grandja +:toc: +:security-site-url: https://projects.spring.io/spring-security/ + +[.lead] +This guide will walk you through the steps for setting up the sample application with OAuth 2.0 Login using an external _OAuth 2.0_ or _OpenID Connect 1.0_ Provider. +The sample application is built with *Spring Boot 1.5* and the *spring-security-oauth2-client* module that is new in {security-site-url}[Spring Security 5.0]. + +The following sections outline detailed steps for setting up OAuth 2.0 Login with these Providers: + +* <> +* <> +* <> +* <> + +NOTE: The _"authentication flow"_ is realized using the *Authorization Code Grant*, as specified in the https://tools.ietf.org/html/rfc6749#section-4.1[OAuth 2.0 Authorization Framework]. + +[[sample-app-content]] +== Sample application content + +The sample application contains the following package structure and artifacts: + +*org.springframework.security.samples* + +[circle] +* _OAuth2LoginApplication_ - the main class for the _Spring application_. +** *user* +*** _GitHubOAuth2User_ - a custom _UserInfo_ type for <>. +** *web* +*** _MainController_ - the root controller that displays user information after a successful login. + +*org.springframework.boot.autoconfigure.security.oauth2.client* + +[circle] +* <> - a Spring Boot auto-configuration class + that automatically registers a _ClientRegistrationRepository_ bean in the _ApplicationContext_. +* <> - a Spring Boot auto-configuration class that automatically enables OAuth 2.0 Login. + +WARNING: The Spring Boot auto-configuration classes (and dependent resources) will eventually _live_ in the *Spring Boot Security Starter*. + +NOTE: See <> for a detailed overview of the auto-configuration classes. + +[[google-login]] +== Setting up *_Login with Google_* + +The goal for this section of the guide is to setup login using Google as the _Authentication Provider_. + +NOTE: https://developers.google.com/identity/protocols/OpenIDConnect[Google's OAuth 2.0 implementation] for authentication conforms to the + http://openid.net/connect/[OpenID Connect] specification and is http://openid.net/certification/[OpenID Certified]. + +[[google-login-register-credentials]] +=== Register OAuth 2.0 credentials + +In order to use Google's OAuth 2.0 authentication system for login, you must set up a project in the *Google API Console* to obtain OAuth 2.0 credentials. + +Follow the instructions on the https://developers.google.com/identity/protocols/OpenIDConnect[OpenID Connect] page starting in the section *_"Setting up OAuth 2.0"_*. + +After completing the sub-section, *_"Obtain OAuth 2.0 credentials"_*, you should have created a new *OAuth Client* with credentials consisting of a *Client ID* and *Client Secret*. + +[[google-login-redirect-uri]] +=== Setting the redirect URI + +The redirect URI is the path in the sample application that the end-user's user-agent is redirected back to after they have authenticated with Google +and have granted access to the OAuth Client _(created from the <>)_ on the *Consent screen* page. + +For the sub-section, *_"Set a redirect URI"_*, ensure the *Authorised redirect URIs* is set to *http://localhost:8080/oauth2/authorize/code/google* + +TIP: The default redirect URI is *_"{scheme}://{serverName}:{serverPort}/oauth2/authorize/code/{clientAlias}"_*. + See <> for more details on this default. + +[[google-login-configure-application-yml]] +=== Configuring application.yml + +Now that we have created a new OAuth Client with Google, we need to configure the sample application to use this OAuth Client for the _authentication flow_. + +Go to *_src/main/resources_* and edit *application.yml*. Add the following configuration: + +[source,yaml] +---- +security: + oauth2: + client: + google: + client-id: ${client-id} + client-secret: ${client-secret} +---- + +Replace *${client-id}* and *${client-secret}* with the OAuth 2.0 credentials created in the previous section <>. + +[TIP] +.OAuth client properties +==== +. *security.oauth2.client* is the *_base property prefix_* for OAuth client properties. +. Just below the *_base property prefix_* is the *_client property key_*, for example *security.oauth2.client.google*. +. At the base of the *_client property key_* are the properties for specifying the configuration for an OAuth Client. + A list of these properties are detailed in <>. +==== + +[[google-login-run-sample]] +=== Running the sample + +Launch the Spring Boot application by running *_org.springframework.security.samples.OAuth2LoginApplication_*. + +After the application successfully starts up, go to http://localhost:8080. You will be redirected to http://localhost:8080/login, which will display an _auto-generated login page_ with an anchor link for *Google*. + +Click through on the Google link and you'll be redirected to Google for authentication. + +After you authenticate using your Google credentials, the next page presented to you will be the *Consent screen*. +The Consent screen will ask you to either *_Allow_* or *_Deny_* access to the OAuth Client you created in the previous step <>. +Click *_Allow_* to authorize the OAuth Client to access your _email address_ and _basic profile_ information. + +At this point, the OAuth Client will retrieve your email address and basic profile information from the http://openid.net/specs/openid-connect-core-1_0.html#UserInfo[*UserInfo Endpoint*] and establish an _authenticated session_. +The home page will then be displayed showing the user attributes retrieved from the *UserInfo Endpoint*, for example, name, email, profile, sub, etc. + +[[oauth2-login-auto-configuration]] +== OAuth 2.0 Login auto-configuration + +As you worked through this guide and setup OAuth 2.0 Login with one of the Providers, +we hope you noticed the ease in configuration and setup required in getting the sample up and running? +And you may be asking, how does this all work? Thanks to some Spring Boot auto-configuration _magic_, +we were able to automatically register the OAuth Client(s) configured in the `Environment`, +as well, provide a minimal security configuration for OAuth 2.0 Login for these registered OAuth Client(s). + +The following provides an overview of the Spring Boot auto-configuration classes: + +[[client-registration-auto-configuration-class]] +*_org.springframework.boot.autoconfigure.security.oauth2.client.ClientRegistrationAutoConfiguration_*:: +`ClientRegistrationAutoConfiguration` is responsible for registering a `ClientRegistrationRepository` _bean_ with the `ApplicationContext`. +The `ClientRegistrationRepository` is composed of one or more `ClientRegistration` instances, which are created from the OAuth client properties +configured in the `Environment` that are prefixed with `security.oauth2.client.[client-alias]`, for example, `security.oauth2.client.google`. + +NOTE: `ClientRegistrationAutoConfiguration` also loads a _resource_ named *oauth2-clients-defaults.yml*, + which provides a set of default client property values for a number of _well-known_ Providers. + More on this in the later section <>. + +[[oauth2-login-auto-configuration-class]] +*_org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2LoginAutoConfiguration_*:: +`OAuth2LoginAutoConfiguration` is responsible for enabling OAuth 2.0 Login, +only if there is a `ClientRegistrationRepository` _bean_ available in the `ApplicationContext`. + +WARNING: The auto-configuration classes (and dependent resources) will eventually _live_ in the *Spring Boot Security Starter*. + +[[oauth2-client-properties]] +=== OAuth client properties + +The following specifies the common set of properties available for configuring an OAuth Client. + +[TIP] +==== +- *security.oauth2.client* is the *_base property prefix_* for OAuth client properties. +- Just below the *_base property prefix_* is the *_client property key_*, for example *security.oauth2.client.google*. +- At the base of the *_client property key_* are the properties for specifying the configuration for an OAuth Client. +==== + +- *client-authentication-method* - the method used to authenticate the _Client_ with the _Provider_. Supported values are *header* and *form*. +- *authorized-grant-type* - the OAuth 2.0 Authorization Framework defines the https://tools.ietf.org/html/rfc6749#section-1.3.1[Authorization Code] grant type, + which is used to realize the _"authentication flow"_. Currently, this is the only supported grant type. +- *redirect-uri* - this is the client's _registered_ redirect URI that the _Authorization Server_ redirects the end-user's user-agent + to after the end-user has authenticated and authorized access for the client. + +NOTE: The default redirect URI is _"{scheme}://{serverName}:{serverPort}/oauth2/authorize/code/{clientAlias}"_, which leverages *URI template variables*. + +- *scopes* - a comma-delimited string of scope(s) requested during the _Authorization Request_ flow, for example: _openid, email, profile_ + +NOTE: _OpenID Connect 1.0_ defines these http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims[standard scopes]: _profile, email, address, phone_ + +NOTE: Non-standard scopes may be defined by a standard _OAuth 2.0 Provider_. Please consult the Provider's OAuth API documentation to learn which scopes are supported. + +- *authorization-uri* - the URI used by the client to redirect the end-user's user-agent to the _Authorization Server_ in order to obtain authorization from the end-user (the _Resource Owner_). +- *token-uri* - the URI used by the client when exchanging an _Authorization Grant_ (for example, Authorization Code) for an _Access Token_ at the _Authorization Server_. +- *user-info-uri* - the URI used by the client to access the protected resource *UserInfo Endpoint*, in order to obtain attributes of the end-user. +- *user-info-converter* - the `Converter` implementation class used to convert the *UserInfo Response* to a `UserInfo` (_OpenID Connect 1.0 Provider_) or `OAuth2User` instance (_Standard OAuth 2.0 Provider_). + +TIP: The `Converter` implementation class for an _OpenID Connect 1.0 Provider_ is *org.springframework.security.oauth2.client.user.converter.UserInfoConverter* + and for a standard _OAuth 2.0 Provider_ it's *org.springframework.security.oauth2.client.user.converter.OAuth2UserConverter*. + +- *user-info-name-attribute-key* - the _key_ used to retrieve the *Name* of the end-user from the `Map` of available attributes in `UserInfo` or `OAuth2User`. + +NOTE: _OpenID Connect 1.0_ defines the http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims[*"name"* Claim], which is the end-user's full name and is the default used for `UserInfo`. + +IMPORTANT: Standard _OAuth 2.0 Provider's_ may vary the naming of their *Name* attribute. Please consult the Provider's *UserInfo* API documentation. + This is a *_required_* property when *user-info-converter* is set to `OAuth2UserConverter`. + +- *client-name* - this is a descriptive name used for the client. The name may be used in certain scenarios, for example, when displaying the name of the client in the _auto-generated login page_. +- *client-alias* - an _alias_ which uniquely identifies the client. It *must be* unique within a `ClientRegistrationRepository`. + +[[oauth2-default-client-properties]] +=== Default client property values + +As noted previously, <> loads a _resource_ named *oauth2-clients-defaults.yml*, +which provides a set of default client property values for a number of _well-known_ Providers. + +For example, the *authorization-uri*, *token-uri*, *user-info-uri* rarely change for a Provider and therefore it makes sense to +provide a set of defaults in order to reduce the configuration required by the user. + +Below are the current set of default client property values: + +.oauth2-clients-defaults.yml +[source,yaml] +---- +security: + oauth2: + client: + google: + client-authentication-method: header + authorized-grant-type: authorization_code + redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}" + scopes: openid, email, profile + authorization-uri: "https://accounts.google.com/o/oauth2/auth" + token-uri: "https://accounts.google.com/o/oauth2/token" + user-info-uri: "https://www.googleapis.com/oauth2/v3/userinfo" + user-info-converter: "org.springframework.security.oauth2.client.user.converter.UserInfoConverter" + client-name: Google + client-alias: google + github: + client-authentication-method: header + authorized-grant-type: authorization_code + redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}" + scopes: user + authorization-uri: "https://github.com/login/oauth/authorize" + token-uri: "https://github.com/login/oauth/access_token" + user-info-uri: "https://api.github.com/user" + user-info-converter: "org.springframework.security.oauth2.client.user.converter.OAuth2UserConverter" + client-name: GitHub + client-alias: github + facebook: + client-authentication-method: form + authorized-grant-type: authorization_code + redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}" + scopes: public_profile, email + authorization-uri: "https://www.facebook.com/v2.8/dialog/oauth" + token-uri: "https://graph.facebook.com/v2.8/oauth/access_token" + user-info-uri: "https://graph.facebook.com/me" + user-info-converter: "org.springframework.security.oauth2.client.user.converter.OAuth2UserConverter" + client-name: Facebook + client-alias: facebook + okta: + client-authentication-method: header + authorized-grant-type: authorization_code + redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}" + scopes: openid, email, profile + user-info-converter: "org.springframework.security.oauth2.client.user.converter.UserInfoConverter" + client-name: Okta + client-alias: okta +---- + += Appendix +''' + +[[configure-non-spring-boot-app]] +== Configuring a _Non-Spring-Boot_ application + +If you are not using Spring Boot for your application, you will not be able to leverage the auto-configuration features for OAuth 2.0 Login. +You will be required to provide your own _security configuration_ in order to enable OAuth 2.0 Login. + +The following sample code demonstrates a minimal security configuration for enabling OAuth 2.0 Login. + +Assuming we have a _properties file_ named *oauth2-clients.properties* on the _classpath_ and it specifies all the _required_ properties for an OAuth Client, specifically _"Google"_: + +.oauth2-clients.properties +[source,properties] +---- +security.oauth2.client.google.client-id=${client-id} +security.oauth2.client.google.client-secret=${client-secret} +security.oauth2.client.google.client-authentication-method=header +security.oauth2.client.google.authorized-grant-type=authorization_code +security.oauth2.client.google.redirect-uri=http://localhost:8080/oauth2/authorize/code/google +security.oauth2.client.google.scopes=openid,email,profile +security.oauth2.client.google.authorization-uri=https://accounts.google.com/o/oauth2/auth +security.oauth2.client.google.token-uri=https://accounts.google.com/o/oauth2/token +security.oauth2.client.google.user-info-uri=https://www.googleapis.com/oauth2/v3/userinfo +security.oauth2.client.google.user-info-converter=org.springframework.security.oauth2.client.user.converter.UserInfoConverter +security.oauth2.client.google.client-name=Google +security.oauth2.client.google.client-alias=google +---- + +The following _security configuration_ will enable OAuth 2.0 Login using _"Google"_ as the _Authentication Provider_: + +[source,java] +---- +@EnableWebSecurity +@PropertySource("classpath:oauth2-clients.properties") +public class SecurityConfig extends WebSecurityConfigurerAdapter { + private Environment environment; + + public SecurityConfig(Environment environment) { + this.environment = environment; + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2Login() + .clients(clientRegistrationRepository()) + .userInfoEndpoint() + .userInfoTypeConverter( + new UserInfoConverter(), + new URI("https://www.googleapis.com/oauth2/v3/userinfo")); + } + + @Bean + public ClientRegistrationRepository clientRegistrationRepository() { + List clientRegistrations = Collections.singletonList( + clientRegistration("security.oauth2.client.google.")); + + return new InMemoryClientRegistrationRepository(clientRegistrations); + } + + private ClientRegistration clientRegistration(String clientPropertyKey) { + String clientId = this.environment.getProperty(clientPropertyKey + "client-id"); + String clientSecret = this.environment.getProperty(clientPropertyKey + "client-secret"); + ClientAuthenticationMethod clientAuthenticationMethod = ClientAuthenticationMethod.valueOf( + this.environment.getProperty(clientPropertyKey + "client-authentication-method").toUpperCase()); + AuthorizationGrantType authorizationGrantType = AuthorizationGrantType.valueOf( + this.environment.getProperty(clientPropertyKey + "authorized-grant-type").toUpperCase()); + String redirectUri = this.environment.getProperty(clientPropertyKey + "redirect-uri"); + String[] scopes = this.environment.getProperty(clientPropertyKey + "scopes").split(","); + String authorizationUri = this.environment.getProperty(clientPropertyKey + "authorization-uri"); + String tokenUri = this.environment.getProperty(clientPropertyKey + "token-uri"); + String userInfoUri = this.environment.getProperty(clientPropertyKey + "user-info-uri"); + String clientName = this.environment.getProperty(clientPropertyKey + "client-name"); + String clientAlias = this.environment.getProperty(clientPropertyKey + "client-alias"); + + return new ClientRegistration.Builder(clientId) + .clientSecret(clientSecret) + .clientAuthenticationMethod(clientAuthenticationMethod) + .authorizedGrantType(authorizationGrantType) + .redirectUri(redirectUri) + .scopes(scopes) + .authorizationUri(authorizationUri) + .tokenUri(tokenUri) + .userInfoUri(userInfoUri) + .clientName(clientName) + .clientAlias(clientAlias) + .build(); + } +} +---- diff --git a/samples/boot/oauth2login/pom.xml b/samples/boot/oauth2login/pom.xml new file mode 100644 index 0000000000..537f636f77 --- /dev/null +++ b/samples/boot/oauth2login/pom.xml @@ -0,0 +1,173 @@ + + + 4.0.0 + org.springframework.security + spring-security-samples-boot-oauth2login + 5.0.0.BUILD-SNAPSHOT + spring-security-samples-boot-oauth2login + spring-security-samples-boot-oauth2login + http://spring.io/spring-security + + spring.io + http://spring.io/ + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + rwinch + Rob Winch + rwinch@pivotal.io + + + jgrandja + Joe Grandja + jgrandja@pivotal.io + + + + scm:git:git://github.com/spring-projects/spring-security + scm:git:git://github.com/spring-projects/spring-security + https://github.com/spring-projects/spring-security + + + + + org.springframework + spring-framework-bom + 4.3.5.RELEASE + pom + import + + + org.springframework.boot + spring-boot-dependencies + 1.5.0.BUILD-SNAPSHOT + pom + import + + + + + + org.springframework.boot + spring-boot-starter-security + compile + + + org.springframework.boot + spring-boot-starter-thymeleaf + compile + + + org.springframework.boot + spring-boot-starter-web + compile + + + org.springframework.security + spring-security-config + 5.0.0.BUILD-SNAPSHOT + compile + + + org.springframework.security + spring-security-oauth2-client + 5.0.0.BUILD-SNAPSHOT + compile + + + org.springframework.security + spring-security-web + 5.0.0.BUILD-SNAPSHOT + compile + + + org.thymeleaf.extras + thymeleaf-extras-springsecurity4 + 2.1.3.RELEASE + compile + + + commons-logging + commons-logging + 1.2 + compile + true + + + ch.qos.logback + logback-classic + 1.1.2 + test + + + junit + junit + 4.12 + test + + + net.sourceforge.htmlunit + htmlunit + 2.24 + test + + + org.assertj + assertj-core + 3.6.2 + test + + + org.mockito + mockito-core + 1.10.19 + test + + + org.slf4j + jcl-over-slf4j + 1.7.7 + test + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + 5.0.0.BUILD-SNAPSHOT + test + + + org.springframework + spring-test + test + + + + + spring-snapshot + https://repo.spring.io/snapshot + + + + + + maven-compiler-plugin + + 1.8 + 1.8 + + + + + diff --git a/samples/boot/oauth2login/spring-security-samples-boot-oauth2login.gradle b/samples/boot/oauth2login/spring-security-samples-boot-oauth2login.gradle new file mode 100644 index 0000000000..08e35c3a74 --- /dev/null +++ b/samples/boot/oauth2login/spring-security-samples-boot-oauth2login.gradle @@ -0,0 +1,15 @@ +apply plugin: 'io.spring.convention.spring-sample-boot' + +dependencies { + compile project(':spring-security-config') + compile project(':spring-security-oauth2-client') + compile project(':spring-security-web') + compile 'org.springframework.boot:spring-boot-starter-security' + compile 'org.springframework.boot:spring-boot-starter-thymeleaf' + compile 'org.springframework.boot:spring-boot-starter-web' + compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity4' + + testCompile project(':spring-security-test') + testCompile 'net.sourceforge.htmlunit:htmlunit' + testCompile 'org.springframework.boot:spring-boot-starter-test' +} diff --git a/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java b/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java new file mode 100644 index 0000000000..07df61415f --- /dev/null +++ b/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java @@ -0,0 +1,405 @@ +/* + * Copyright 2012-2017 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 com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.WebResponse; +import com.gargoylesoftware.htmlunit.html.DomNodeList; +import com.gargoylesoftware.htmlunit.html.HtmlAnchor; +import com.gargoylesoftware.htmlunit.html.HtmlElement; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import org.junit.Before; +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.http.HttpStatus; +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.authentication.AuthorizationCodeAuthenticationProcessingFilter; +import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationToken; +import org.springframework.security.oauth2.client.authentication.AuthorizationCodeRequestRedirectFilter; +import org.springframework.security.oauth2.client.authentication.AuthorizationGrantTokenExchanger; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.user.OAuth2UserService; +import org.springframework.security.oauth2.core.AccessToken; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.endpoint.OAuth2Parameter; +import org.springframework.security.oauth2.core.endpoint.ResponseType; +import org.springframework.security.oauth2.core.endpoint.TokenResponseAttributes; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; +import java.net.URL; +import java.net.URLDecoder; +import java.util.*; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Integration tests for the OAuth 2.0 client filters {@link AuthorizationCodeRequestRedirectFilter} + * and {@link AuthorizationCodeAuthenticationProcessingFilter}. + * These filters work together to realize the Authorization Code Grant flow. + * + * @author Joe Grandja + */ +@RunWith(SpringRunner.class) +@SpringBootTest +@AutoConfigureMockMvc +public class OAuth2LoginApplicationTests { + private static final String AUTHORIZATION_BASE_URI = "/oauth2/authorization/code"; + private static final String AUTHORIZE_BASE_URL = "http://localhost:8080/oauth2/authorize/code"; + + @Autowired + private WebClient webClient; + + @Autowired + private ClientRegistrationRepository clientRegistrationRepository; + + private ClientRegistration googleClientRegistration; + private ClientRegistration githubClientRegistration; + private ClientRegistration facebookClientRegistration; + private ClientRegistration oktaClientRegistration; + + @Before + public void setup() { + this.webClient.getCookieManager().clearCookies(); + this.googleClientRegistration = this.clientRegistrationRepository.getRegistrationByClientAlias("google"); + this.githubClientRegistration = this.clientRegistrationRepository.getRegistrationByClientAlias("github"); + this.facebookClientRegistration = this.clientRegistrationRepository.getRegistrationByClientAlias("facebook"); + this.oktaClientRegistration = this.clientRegistrationRepository.getRegistrationByClientAlias("okta"); + } + + @Test + public void requestRootPageWhenNotAuthenticatedThenDisplayLoginPage() throws Exception { + HtmlPage page = this.webClient.getPage("/"); + this.assertLoginPage(page); + } + + @Test + public void requestOtherPageWhenNotAuthenticatedThenDisplayLoginPage() throws Exception { + HtmlPage page = this.webClient.getPage("/other-page"); + this.assertLoginPage(page); + } + + @Test + public void requestAuthorizeGitHubClientWhenLinkClickedThenStatusRedirectForAuthorization() throws Exception { + HtmlPage page = this.webClient.getPage("/"); + + HtmlAnchor clientAnchorElement = this.getClientAnchorElement(page, this.githubClientRegistration); + assertThat(clientAnchorElement).isNotNull(); + + WebResponse response = this.followLinkDisableRedirects(clientAnchorElement); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.MOVED_PERMANENTLY.value()); + + String authorizeRedirectUri = response.getResponseHeaderValue("Location"); + assertThat(authorizeRedirectUri).isNotNull(); + + UriComponents uriComponents = UriComponentsBuilder.fromUri(URI.create(authorizeRedirectUri)).build(); + + String requestUri = uriComponents.getScheme() + "://" + uriComponents.getHost() + uriComponents.getPath(); + assertThat(requestUri).isEqualTo(this.githubClientRegistration.getProviderDetails().getAuthorizationUri().toString()); + + Map params = uriComponents.getQueryParams().toSingleValueMap(); + + assertThat(params.get(OAuth2Parameter.RESPONSE_TYPE)).isEqualTo(ResponseType.CODE.value()); + assertThat(params.get(OAuth2Parameter.CLIENT_ID)).isEqualTo(this.githubClientRegistration.getClientId()); + String redirectUri = AUTHORIZE_BASE_URL + "/" + this.githubClientRegistration.getClientAlias(); + assertThat(URLDecoder.decode(params.get(OAuth2Parameter.REDIRECT_URI), "UTF-8")).isEqualTo(redirectUri); + assertThat(URLDecoder.decode(params.get(OAuth2Parameter.SCOPE), "UTF-8")) + .isEqualTo(this.githubClientRegistration.getScopes().stream().collect(Collectors.joining(" "))); + assertThat(params.get(OAuth2Parameter.STATE)).isNotNull(); + } + + @Test + public void requestAuthorizeClientWhenInvalidClientThenStatusBadRequest() throws Exception { + HtmlPage page = this.webClient.getPage("/"); + + HtmlAnchor clientAnchorElement = this.getClientAnchorElement(page, this.googleClientRegistration); + assertThat(clientAnchorElement).isNotNull(); + clientAnchorElement.setAttribute("href", clientAnchorElement.getHrefAttribute() + "-invalid"); + + WebResponse response = null; + try { + clientAnchorElement.click(); + } catch (FailingHttpStatusCodeException ex) { + response = ex.getResponse(); + } + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + } + + @Test + public void requestAuthorizationCodeGrantWhenValidAuthorizationResponseThenDisplayUserInfoPage() throws Exception { + HtmlPage page = this.webClient.getPage("/"); + + HtmlAnchor clientAnchorElement = this.getClientAnchorElement(page, this.githubClientRegistration); + assertThat(clientAnchorElement).isNotNull(); + + WebResponse response = this.followLinkDisableRedirects(clientAnchorElement); + + UriComponents authorizeRequestUriComponents = UriComponentsBuilder.fromUri( + URI.create(response.getResponseHeaderValue("Location"))).build(); + + Map params = authorizeRequestUriComponents.getQueryParams().toSingleValueMap(); + String code = "auth-code"; + String state = URLDecoder.decode(params.get(OAuth2Parameter.STATE), "UTF-8"); + String redirectUri = URLDecoder.decode(params.get(OAuth2Parameter.REDIRECT_URI), "UTF-8"); + + String authorizationResponseUri = + UriComponentsBuilder.fromHttpUrl(redirectUri) + .queryParam(OAuth2Parameter.CODE, code) + .queryParam(OAuth2Parameter.STATE, state) + .build().encode().toUriString(); + + page = this.webClient.getPage(new URL(authorizationResponseUri)); + this.assertUserInfoPage(page); + } + + @Test + public void requestAuthorizationCodeGrantWhenNoMatchingAuthorizationRequestThenDisplayLoginPageWithError() throws Exception { + HtmlPage page = this.webClient.getPage("/"); + URL loginPageUrl = page.getBaseURL(); + URL loginErrorPageUrl = new URL(loginPageUrl.toString() + "?error"); + + String code = "auth-code"; + String state = "state"; + String redirectUri = AUTHORIZE_BASE_URL + "/" + this.googleClientRegistration.getClientAlias(); + + String authorizationResponseUri = + UriComponentsBuilder.fromHttpUrl(redirectUri) + .queryParam(OAuth2Parameter.CODE, code) + .queryParam(OAuth2Parameter.STATE, state) + .build().encode().toUriString(); + + // Clear session cookie will ensure the 'session-saved' + // Authorization Request (from previous request) is not found + this.webClient.getCookieManager().clearCookies(); + + page = this.webClient.getPage(new URL(authorizationResponseUri)); + assertThat(page.getBaseURL()).isEqualTo(loginErrorPageUrl); + + HtmlElement errorElement = page.getBody().getFirstByXPath("p"); + assertThat(errorElement).isNotNull(); + assertThat(errorElement.asText()).contains("authorization_request_not_found"); + } + + @Test + public void requestAuthorizationCodeGrantWhenInvalidStateParamThenDisplayLoginPageWithError() throws Exception { + HtmlPage page = this.webClient.getPage("/"); + URL loginPageUrl = page.getBaseURL(); + URL loginErrorPageUrl = new URL(loginPageUrl.toString() + "?error"); + + HtmlAnchor clientAnchorElement = this.getClientAnchorElement(page, this.googleClientRegistration); + assertThat(clientAnchorElement).isNotNull(); + this.followLinkDisableRedirects(clientAnchorElement); + + String code = "auth-code"; + String state = "invalid-state"; + String redirectUri = AUTHORIZE_BASE_URL + "/" + this.githubClientRegistration.getClientAlias(); + + String authorizationResponseUri = + UriComponentsBuilder.fromHttpUrl(redirectUri) + .queryParam(OAuth2Parameter.CODE, code) + .queryParam(OAuth2Parameter.STATE, state) + .build().encode().toUriString(); + + page = this.webClient.getPage(new URL(authorizationResponseUri)); + assertThat(page.getBaseURL()).isEqualTo(loginErrorPageUrl); + + HtmlElement errorElement = page.getBody().getFirstByXPath("p"); + assertThat(errorElement).isNotNull(); + assertThat(errorElement.asText()).contains("invalid_state_parameter"); + } + + @Test + public void requestAuthorizationCodeGrantWhenInvalidRedirectUriThenDisplayLoginPageWithError() throws Exception { + HtmlPage page = this.webClient.getPage("/"); + URL loginPageUrl = page.getBaseURL(); + URL loginErrorPageUrl = new URL(loginPageUrl.toString() + "?error"); + + HtmlAnchor clientAnchorElement = this.getClientAnchorElement(page, this.googleClientRegistration); + assertThat(clientAnchorElement).isNotNull(); + + WebResponse response = this.followLinkDisableRedirects(clientAnchorElement); + + UriComponents authorizeRequestUriComponents = UriComponentsBuilder.fromUri( + URI.create(response.getResponseHeaderValue("Location"))).build(); + + Map params = authorizeRequestUriComponents.getQueryParams().toSingleValueMap(); + String code = "auth-code"; + String state = URLDecoder.decode(params.get(OAuth2Parameter.STATE), "UTF-8"); + String redirectUri = URLDecoder.decode(params.get(OAuth2Parameter.REDIRECT_URI), "UTF-8"); + redirectUri += "-invalid"; + + String authorizationResponseUri = + UriComponentsBuilder.fromHttpUrl(redirectUri) + .queryParam(OAuth2Parameter.CODE, code) + .queryParam(OAuth2Parameter.STATE, state) + .build().encode().toUriString(); + + page = this.webClient.getPage(new URL(authorizationResponseUri)); + assertThat(page.getBaseURL()).isEqualTo(loginErrorPageUrl); + + HtmlElement errorElement = page.getBody().getFirstByXPath("p"); + assertThat(errorElement).isNotNull(); + assertThat(errorElement.asText()).contains("invalid_redirect_uri_parameter"); + } + + @Test + public void requestAuthorizationCodeGrantWhenStandardErrorCodeResponseThenDisplayLoginPageWithError() throws Exception { + HtmlPage page = this.webClient.getPage("/"); + URL loginPageUrl = page.getBaseURL(); + URL loginErrorPageUrl = new URL(loginPageUrl.toString() + "?error"); + + String error = OAuth2Error.INVALID_CLIENT_ERROR_CODE; + String state = "state"; + String redirectUri = AUTHORIZE_BASE_URL + "/" + this.githubClientRegistration.getClientAlias(); + + String authorizationResponseUri = + UriComponentsBuilder.fromHttpUrl(redirectUri) + .queryParam(OAuth2Parameter.ERROR, error) + .queryParam(OAuth2Parameter.STATE, state) + .build().encode().toUriString(); + + page = this.webClient.getPage(new URL(authorizationResponseUri)); + assertThat(page.getBaseURL()).isEqualTo(loginErrorPageUrl); + + HtmlElement errorElement = page.getBody().getFirstByXPath("p"); + assertThat(errorElement).isNotNull(); + assertThat(errorElement.asText()).contains(error); + } + + private void assertLoginPage(HtmlPage page) throws Exception { + assertThat(page.getTitleText()).isEqualTo("Login Page"); + + int expectedClients = 4; + + List clientAnchorElements = page.getAnchors(); + assertThat(clientAnchorElements.size()).isEqualTo(expectedClients); + + String baseAuthorizeUri = AUTHORIZATION_BASE_URI + "/"; + String googleClientAuthorizeUri = baseAuthorizeUri + this.googleClientRegistration.getClientAlias(); + String githubClientAuthorizeUri = baseAuthorizeUri + this.githubClientRegistration.getClientAlias(); + String facebookClientAuthorizeUri = baseAuthorizeUri + this.facebookClientRegistration.getClientAlias(); + String oktaClientAuthorizeUri = baseAuthorizeUri + this.oktaClientRegistration.getClientAlias(); + + for (int i=0; i divElements = page.getBody().getElementsByTagName("div"); + assertThat(divElements.get(1).asText()).contains("User: joeg@springsecurity.io"); + assertThat(divElements.get(4).asText()).contains("Name: joeg@springsecurity.io"); + } + + private HtmlAnchor getClientAnchorElement(HtmlPage page, ClientRegistration clientRegistration) { + Optional clientAnchorElement = page.getAnchors().stream() + .filter(e -> e.asText().equals(clientRegistration.getClientName())).findFirst(); + + return (clientAnchorElement.isPresent() ? clientAnchorElement.get() : null); + } + + private WebResponse followLinkDisableRedirects(HtmlAnchor anchorElement) throws Exception { + WebResponse response = null; + try { + // Disable the automatic redirection (which will trigger + // an exception) so that we can capture the response + this.webClient.getOptions().setRedirectEnabled(false); + anchorElement.click(); + } catch (FailingHttpStatusCodeException ex) { + response = ex.getResponse(); + this.webClient.getOptions().setRedirectEnabled(true); + } + return response; + } + + @EnableWebSecurity + public static class SecurityTestConfig extends WebSecurityConfigurerAdapter { + + // @formatter:off + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2Login() + .authorizationCodeTokenExchanger(this.mockAuthorizationCodeTokenExchanger()) + .userInfoEndpoint() + .userInfoService(this.mockUserInfoService()); + } + // @formatter:on + + private AuthorizationGrantTokenExchanger mockAuthorizationCodeTokenExchanger() { + TokenResponseAttributes tokenResponse = TokenResponseAttributes.withToken("access-token-1234") + .tokenType(AccessToken.TokenType.BEARER) + .expiresIn(60 * 1000) + .scopes(Collections.singleton("openid")) + .build(); + + AuthorizationGrantTokenExchanger mock = mock(AuthorizationGrantTokenExchanger.class); + when(mock.exchange(any())).thenReturn(tokenResponse); + return mock; + } + + private OAuth2UserService mockUserInfoService() { + Map attributes = new HashMap<>(); + attributes.put("id", "joeg"); + attributes.put("first-name", "Joe"); + attributes.put("last-name", "Grandja"); + attributes.put("email", "joeg@springsecurity.io"); + + DefaultOAuth2User user = new DefaultOAuth2User(attributes, "email"); + + OAuth2UserService mock = mock(OAuth2UserService.class); + when(mock.loadUser(any())).thenReturn(user); + return mock; + } + } + + @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan(basePackages = "org.springframework.security.samples.web") + public static class SpringBootApplicationTestConfig { + } +} diff --git a/samples/boot/oauth2login/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ClientRegistrationAutoConfiguration.java b/samples/boot/oauth2login/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ClientRegistrationAutoConfiguration.java new file mode 100644 index 0000000000..5f1c952c07 --- /dev/null +++ b/samples/boot/oauth2login/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ClientRegistrationAutoConfiguration.java @@ -0,0 +1,129 @@ +/* + * Copyright 2012-2017 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.boot.autoconfigure.security.oauth2.client; + +import org.springframework.beans.factory.config.YamlPropertiesFactoryBean; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.*; +import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; +import org.springframework.boot.bind.PropertySourcesBinder; +import org.springframework.boot.bind.RelaxedPropertyResolver; +import org.springframework.context.annotation.*; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertiesPropertySource; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationProperties; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; +import org.springframework.util.CollectionUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author Joe Grandja + */ +@Configuration +@ConditionalOnWebApplication +@ConditionalOnClass(ClientRegistrationRepository.class) +@ConditionalOnMissingBean(ClientRegistrationRepository.class) +@AutoConfigureBefore(SecurityAutoConfiguration.class) +public class ClientRegistrationAutoConfiguration { + private static final String CLIENT_ID_PROPERTY = "client-id"; + private static final String CLIENTS_DEFAULTS_RESOURCE = "META-INF/oauth2-clients-defaults.yml"; + static final String CLIENT_PROPERTY_PREFIX = "security.oauth2.client."; + + @Configuration + @Conditional(ClientPropertiesAvailableCondition.class) + protected static class ClientRegistrationConfiguration { + private final Environment environment; + + protected ClientRegistrationConfiguration(Environment environment) { + this.environment = environment; + } + + @Bean + public ClientRegistrationRepository clientRegistrationRepository() { + MutablePropertySources propertySources = ((ConfigurableEnvironment) this.environment).getPropertySources(); + Properties clientsDefaultProperties = this.getClientsDefaultProperties(); + if (clientsDefaultProperties != null) { + propertySources.addLast(new PropertiesPropertySource("oauth2ClientsDefaults", clientsDefaultProperties)); + } + PropertySourcesBinder binder = new PropertySourcesBinder(propertySources); + RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(this.environment, CLIENT_PROPERTY_PREFIX); + + List clientRegistrations = new ArrayList<>(); + + Set clientPropertyKeys = resolveClientPropertyKeys(this.environment); + for (String clientPropertyKey : clientPropertyKeys) { + if (!resolver.containsProperty(clientPropertyKey + "." + CLIENT_ID_PROPERTY)) { + continue; + } + ClientRegistrationProperties clientRegistrationProperties = new ClientRegistrationProperties(); + binder.bindTo(CLIENT_PROPERTY_PREFIX + clientPropertyKey, clientRegistrationProperties); + ClientRegistration clientRegistration = new ClientRegistration.Builder(clientRegistrationProperties).build(); + clientRegistrations.add(clientRegistration); + } + + return new InMemoryClientRegistrationRepository(clientRegistrations); + } + + private Properties getClientsDefaultProperties() { + ClassPathResource clientsDefaultsResource = new ClassPathResource(CLIENTS_DEFAULTS_RESOURCE); + if (!clientsDefaultsResource.exists()) { + return null; + } + YamlPropertiesFactoryBean yamlPropertiesFactory = new YamlPropertiesFactoryBean(); + yamlPropertiesFactory.setResources(clientsDefaultsResource); + return yamlPropertiesFactory.getObject(); + } + } + + static Set resolveClientPropertyKeys(Environment environment) { + Set clientPropertyKeys = new LinkedHashSet<>(); + RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(environment, CLIENT_PROPERTY_PREFIX); + resolver.getSubProperties("").keySet().forEach(key -> { + int endIndex = key.indexOf('.'); + if (endIndex != -1) { + clientPropertyKeys.add(key.substring(0, endIndex)); + } + }); + return clientPropertyKeys; + } + + private static class ClientPropertiesAvailableCondition extends SpringBootCondition implements ConfigurationCondition { + + @Override + public ConfigurationCondition.ConfigurationPhase getConfigurationPhase() { + return ConfigurationPhase.PARSE_CONFIGURATION; + } + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + ConditionMessage.Builder message = ConditionMessage.forCondition("OAuth2 Client Properties"); + Set clientPropertyKeys = resolveClientPropertyKeys(context.getEnvironment()); + if (!CollectionUtils.isEmpty(clientPropertyKeys)) { + return ConditionOutcome.match(message.foundExactly("OAuth2 Client(s) -> " + + clientPropertyKeys.stream().collect(Collectors.joining(", ")))); + } + return ConditionOutcome.noMatch(message.notAvailable("OAuth2 Client(s)")); + } + } +} diff --git a/samples/boot/oauth2login/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2LoginAutoConfiguration.java b/samples/boot/oauth2login/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2LoginAutoConfiguration.java new file mode 100644 index 0000000000..0e80a457f4 --- /dev/null +++ b/samples/boot/oauth2login/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2LoginAutoConfiguration.java @@ -0,0 +1,108 @@ +/* + * Copyright 2012-2017 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.boot.autoconfigure.security.oauth2.client; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.env.Environment; +import org.springframework.http.client.ClientHttpResponse; +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.WebSecurityConfiguration; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.user.converter.AbstractOAuth2UserConverter; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.util.ClassUtils; + +import java.lang.reflect.Constructor; +import java.net.URI; +import java.util.Set; + +import static org.springframework.boot.autoconfigure.security.oauth2.client.ClientRegistrationAutoConfiguration.CLIENT_PROPERTY_PREFIX; +import static org.springframework.boot.autoconfigure.security.oauth2.client.ClientRegistrationAutoConfiguration.resolveClientPropertyKeys; + +/** + * @author Joe Grandja + */ +@Configuration +@ConditionalOnWebApplication +@ConditionalOnClass(EnableWebSecurity.class) +@ConditionalOnMissingBean(WebSecurityConfiguration.class) +@ConditionalOnBean(ClientRegistrationRepository.class) +@AutoConfigureBefore(SecurityAutoConfiguration.class) +@AutoConfigureAfter(ClientRegistrationAutoConfiguration.class) +public class OAuth2LoginAutoConfiguration { + private static final String USER_INFO_URI_PROPERTY = "user-info-uri"; + private static final String USER_INFO_CONVERTER_PROPERTY = "user-info-converter"; + private static final String USER_INFO_NAME_ATTR_KEY_PROPERTY = "user-info-name-attribute-key"; + + @EnableWebSecurity + protected static class OAuth2LoginSecurityConfiguration extends WebSecurityConfigurerAdapter { + private final Environment environment; + + protected OAuth2LoginSecurityConfiguration(Environment environment) { + this.environment = environment; + } + + // @formatter:off + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeRequests() + .antMatchers("/favicon.ico").permitAll() + .anyRequest().authenticated() + .and() + .oauth2Login(); + + this.registerUserInfoTypeConverters(http.oauth2Login()); + } + // @formatter:on + + private void registerUserInfoTypeConverters(OAuth2LoginConfigurer oauth2LoginConfigurer) throws Exception { + Set clientPropertyKeys = resolveClientPropertyKeys(this.environment); + for (String clientPropertyKey : clientPropertyKeys) { + String fullClientPropertyKey = CLIENT_PROPERTY_PREFIX + clientPropertyKey + "."; + String userInfoUriValue = this.environment.getProperty(fullClientPropertyKey + USER_INFO_URI_PROPERTY); + String userInfoConverterTypeValue = this.environment.getProperty(fullClientPropertyKey + USER_INFO_CONVERTER_PROPERTY); + if (userInfoUriValue != null && userInfoConverterTypeValue != null) { + Class userInfoConverterType = ClassUtils.resolveClassName( + userInfoConverterTypeValue, this.getClass().getClassLoader()).asSubclass(Converter.class); + Converter userInfoConverter = null; + if (AbstractOAuth2UserConverter.class.isAssignableFrom(userInfoConverterType)) { + Constructor oauth2UserConverterConstructor = ClassUtils.getConstructorIfAvailable(userInfoConverterType, String.class); + if (oauth2UserConverterConstructor != null) { + String userInfoNameAttributeKey = this.environment.getProperty(fullClientPropertyKey + USER_INFO_NAME_ATTR_KEY_PROPERTY); + userInfoConverter = (Converter)oauth2UserConverterConstructor.newInstance(userInfoNameAttributeKey); + } + } + if (userInfoConverter == null) { + userInfoConverter = (Converter)userInfoConverterType.newInstance(); + } + oauth2LoginConfigurer.userInfoEndpoint().userInfoTypeConverter(userInfoConverter, new URI(userInfoUriValue)); + } + } + } + } +} diff --git a/samples/boot/oauth2login/src/main/java/org/springframework/security/samples/OAuth2LoginApplication.java b/samples/boot/oauth2login/src/main/java/org/springframework/security/samples/OAuth2LoginApplication.java new file mode 100644 index 0000000000..3f70a8b17e --- /dev/null +++ b/samples/boot/oauth2login/src/main/java/org/springframework/security/samples/OAuth2LoginApplication.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2017 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.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Joe Grandja + */ +@SpringBootApplication +public class OAuth2LoginApplication { + + public OAuth2LoginApplication() { + } + + public static void main(String[] args) { + SpringApplication.run(OAuth2LoginApplication.class, args); + } + +} diff --git a/samples/boot/oauth2login/src/main/java/org/springframework/security/samples/user/GitHubOAuth2User.java b/samples/boot/oauth2login/src/main/java/org/springframework/security/samples/user/GitHubOAuth2User.java new file mode 100644 index 0000000000..10254cc579 --- /dev/null +++ b/samples/boot/oauth2login/src/main/java/org/springframework/security/samples/user/GitHubOAuth2User.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012-2017 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.user; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * @author Joe Grandja + */ +public class GitHubOAuth2User implements OAuth2User { + private String id; + private String name; + private String login; + private String email; + + public GitHubOAuth2User() { + } + + @Override + public Collection getAuthorities() { + return Collections.emptyList(); + } + + @Override + public Map getAttributes() { + Map attributes = new HashMap<>(); + attributes.put("id", this.getId()); + attributes.put("name", this.getName()); + attributes.put("login", this.getLogin()); + attributes.put("email", this.getEmail()); + return attributes; + } + + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + @Override + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public String getLogin() { + return this.login; + } + + public void setLogin(String login) { + this.login = login; + } + + public String getEmail() { + return this.email; + } + + public void setEmail(String email) { + this.email = email; + } +} diff --git a/samples/boot/oauth2login/src/main/java/org/springframework/security/samples/web/MainController.java b/samples/boot/oauth2login/src/main/java/org/springframework/security/samples/web/MainController.java new file mode 100644 index 0000000000..8fdb843022 --- /dev/null +++ b/samples/boot/oauth2login/src/main/java/org/springframework/security/samples/web/MainController.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2017 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.web; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * @author Joe Grandja + */ +@Controller +public class MainController { + + @RequestMapping("/") + public String index(Model model, @AuthenticationPrincipal OAuth2User user) { + model.addAttribute("userName", user.getName()); + model.addAttribute("userAttributes", user.getAttributes()); + return "index"; + } +} diff --git a/samples/boot/oauth2login/src/main/resources/META-INF/oauth2-clients-defaults.yml b/samples/boot/oauth2login/src/main/resources/META-INF/oauth2-clients-defaults.yml new file mode 100644 index 0000000000..6ad5ed450a --- /dev/null +++ b/samples/boot/oauth2login/src/main/resources/META-INF/oauth2-clients-defaults.yml @@ -0,0 +1,44 @@ +security: + oauth2: + client: + google: + client-authentication-method: header + authorized-grant-type: authorization_code + redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}" + scopes: openid, email, profile + authorization-uri: "https://accounts.google.com/o/oauth2/auth" + token-uri: "https://accounts.google.com/o/oauth2/token" + user-info-uri: "https://www.googleapis.com/oauth2/v3/userinfo" + user-info-converter: "org.springframework.security.oauth2.client.user.converter.UserInfoConverter" + client-name: Google + client-alias: google + github: + client-authentication-method: header + authorized-grant-type: authorization_code + redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}" + scopes: user + authorization-uri: "https://github.com/login/oauth/authorize" + token-uri: "https://github.com/login/oauth/access_token" + user-info-uri: "https://api.github.com/user" + user-info-converter: "org.springframework.security.oauth2.client.user.converter.OAuth2UserConverter" + client-name: GitHub + client-alias: github + facebook: + client-authentication-method: form + authorized-grant-type: authorization_code + redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}" + scopes: public_profile, email + authorization-uri: "https://www.facebook.com/v2.8/dialog/oauth" + token-uri: "https://graph.facebook.com/v2.8/oauth/access_token" + user-info-uri: "https://graph.facebook.com/me" + user-info-converter: "org.springframework.security.oauth2.client.user.converter.OAuth2UserConverter" + client-name: Facebook + client-alias: facebook + okta: + client-authentication-method: header + authorized-grant-type: authorization_code + redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}" + scopes: openid, email, profile + user-info-converter: "org.springframework.security.oauth2.client.user.converter.UserInfoConverter" + client-name: Okta + client-alias: okta diff --git a/samples/boot/oauth2login/src/main/resources/META-INF/spring.factories b/samples/boot/oauth2login/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..de3be686f9 --- /dev/null +++ b/samples/boot/oauth2login/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +# Spring Boot Auto Configurations +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.boot.autoconfigure.security.oauth2.client.ClientRegistrationAutoConfiguration,\ +org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2LoginAutoConfiguration diff --git a/samples/boot/oauth2login/src/main/resources/application.yml b/samples/boot/oauth2login/src/main/resources/application.yml new file mode 100644 index 0000000000..faa25818f7 --- /dev/null +++ b/samples/boot/oauth2login/src/main/resources/application.yml @@ -0,0 +1,34 @@ +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: + google: + client-id: your-app-client-id + client-secret: your-app-client-secret + github: + client-id: your-app-client-id + client-secret: your-app-client-secret + user-info-name-attribute-key: "name" + facebook: + client-id: your-app-client-id + client-secret: your-app-client-secret + user-info-name-attribute-key: "name" + okta: + client-id: your-app-client-id + client-secret: your-app-client-secret + authorization-uri: https://your-subdomain.oktapreview.com/oauth2/v1/authorize + token-uri: https://your-subdomain.oktapreview.com/oauth2/v1/token + user-info-uri: https://your-subdomain.oktapreview.com/oauth2/v1/userinfo diff --git a/samples/boot/oauth2login/src/main/resources/templates/index.html b/samples/boot/oauth2login/src/main/resources/templates/index.html new file mode 100644 index 0000000000..cd7c31f9d1 --- /dev/null +++ b/samples/boot/oauth2login/src/main/resources/templates/index.html @@ -0,0 +1,33 @@ + + + + Spring Security - OAuth2 User Info + + + +

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

OAuth2 User Info

+
+ Name: +
+
 
+
+ Attributes: +
    +
  • + : +
  • +
+
+ + diff --git a/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java b/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java index 8561b6d559..00b0488172 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java @@ -15,7 +15,13 @@ */ package org.springframework.security.web.authentication.ui; -import java.io.IOException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.WebAttributes; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.web.filter.GenericFilterBean; import javax.servlet.FilterChain; import javax.servlet.ServletException; @@ -24,14 +30,8 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; - -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.WebAttributes; -import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices; -import org.springframework.security.web.csrf.CsrfToken; -import org.springframework.web.filter.GenericFilterBean; +import java.io.IOException; +import java.util.Map; /** * For internal use with namespace configuration in the case where a user doesn't @@ -51,6 +51,7 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean { private String failureUrl; private boolean formLoginEnabled; private boolean openIdEnabled; + private boolean oauth2LoginEnabled; private String authenticationUrl; private String usernameParameter; private String passwordParameter; @@ -58,6 +59,8 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean { private String openIDauthenticationUrl; private String openIDusernameParameter; private String openIDrememberMeParameter; + private Map oauth2AuthenticationUrlToClientName; + public DefaultLoginPageGeneratingFilter() { } @@ -105,7 +108,7 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean { } public boolean isEnabled() { - return formLoginEnabled || openIdEnabled; + return formLoginEnabled || openIdEnabled || oauth2LoginEnabled; } public void setLogoutSuccessUrl(String logoutSuccessUrl) { @@ -132,6 +135,10 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean { this.openIdEnabled = openIdEnabled; } + public void setOauth2LoginEnabled(boolean oauth2LoginEnabled) { + this.oauth2LoginEnabled = oauth2LoginEnabled; + } + public void setAuthenticationUrl(String authenticationUrl) { this.authenticationUrl = authenticationUrl; } @@ -157,6 +164,10 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean { this.openIDusernameParameter = openIDusernameParameter; } + public void setOauth2AuthenticationUrlToClientName(Map oauth2AuthenticationUrlToClientName) { + this.oauth2AuthenticationUrlToClientName = oauth2AuthenticationUrlToClientName; + } + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; @@ -201,13 +212,13 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean { } if (loginError) { - sb.append("

Your login attempt was not successful, try again.

Reason: "); + sb.append("

Your login attempt was not successful, try again.

Reason: "); sb.append(errorMsg); - sb.append("

"); + sb.append("

"); } if (logoutSuccess) { - sb.append("

You have been logged out

"); + sb.append("

You have been logged out

"); } if (formLoginEnabled) { @@ -252,6 +263,19 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean { sb.append(""); } + if (oauth2LoginEnabled) { + sb.append("

Login with OAuth 2.0

"); + sb.append("
User:
Password:
\n"); + for (Map.Entry clientAuthenticationUrlToClientName : oauth2AuthenticationUrlToClientName.entrySet()) { + sb.append(" \n"); + } + sb.append("
"); + sb.append(""); + sb.append(clientAuthenticationUrlToClientName.getValue()); + sb.append(""); + sb.append("
\n"); + } + sb.append(""); return sb.toString();