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

Add support for OAuth 2.0 Login

Fixes gh-3907
Joe Grandja 8 жил өмнө
parent
commit
829c386756
81 өөрчлөгдсөн 6485 нэмэгдсэн , 50 устгасан
  1. 7 0
      config/pom.xml
  2. 1 0
      config/spring-security-config.gradle
  3. 8 0
      config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java
  4. 153 0
      config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java
  5. 123 0
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/AuthorizationCodeAuthenticationFilterConfigurer.java
  6. 61 0
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/AuthorizationCodeRequestRedirectFilterConfigurer.java
  7. 154 0
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java
  8. 2 2
      config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.groovy
  9. 8 35
      docs/manual/src/docs/asciidoc/index.adoc
  10. 156 0
      oauth2/oauth2-client/pom.xml
  11. 12 0
      oauth2/oauth2-client/spring-security-oauth2-client.gradle
  12. 225 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProcessingFilter.java
  13. 120 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProvider.java
  14. 63 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationToken.java
  15. 159 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeRequestRedirectFilter.java
  16. 50 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationGrantAuthenticationToken.java
  17. 40 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationGrantTokenExchanger.java
  18. 45 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationRequestRepository.java
  19. 31 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationRequestUriBuilder.java
  20. 49 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/DefaultAuthorizationRequestUriBuilder.java
  21. 54 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/DefaultStateGenerator.java
  22. 63 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/HttpSessionAuthorizationRequestRepository.java
  23. 66 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationException.java
  24. 87 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationToken.java
  25. 142 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/nimbus/NimbusAuthorizationCodeTokenExchanger.java
  26. 279 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java
  27. 129 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrationProperties.java
  28. 33 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrationRepository.java
  29. 59 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/InMemoryClientRegistrationRepository.java
  30. 42 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/OAuth2UserService.java
  31. 53 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/AbstractOAuth2UserConverter.java
  32. 51 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/CustomOAuth2UserConverter.java
  33. 41 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/OAuth2UserConverter.java
  34. 34 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/UserInfoConverter.java
  35. 69 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/nimbus/NimbusClientHttpResponse.java
  36. 121 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/nimbus/NimbusOAuth2UserService.java
  37. 45 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/converter/AuthorizationCodeAuthorizationResponseAttributesConverter.java
  38. 53 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/converter/ErrorResponseAttributesConverter.java
  39. 254 0
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProcessingFilterTests.java
  40. 142 0
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeRequestRedirectFilterTests.java
  41. 81 0
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/TestUtil.java
  42. 138 0
      oauth2/oauth2-core/pom.xml
  43. 9 0
      oauth2/oauth2-core/spring-security-oauth2-core.gradle
  44. 59 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AbstractToken.java
  45. 88 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AccessToken.java
  46. 46 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java
  47. 38 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClientAuthenticationMethod.java
  48. 74 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2Error.java
  49. 44 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/AuthorizationCodeAuthorizationResponseAttributes.java
  50. 76 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/AuthorizationCodeTokenRequestAttributes.java
  51. 127 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/AuthorizationRequestAttributes.java
  52. 96 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/ErrorResponseAttributes.java
  53. 46 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2Parameter.java
  54. 46 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/ResponseType.java
  55. 109 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/TokenResponseAttributes.java
  56. 21 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/package-info.java
  57. 19 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/package-info.java
  58. 153 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/DefaultOAuth2User.java
  59. 55 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/OAuth2User.java
  60. 19 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/package-info.java
  61. 69 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/StandardClaimName.java
  62. 19 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/package-info.java
  63. 154 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/DefaultUserInfo.java
  64. 103 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/UserInfo.java
  65. 19 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/package-info.java
  66. 7 0
      samples/boot/helloworld/pom.xml
  67. 7 0
      samples/boot/insecure/pom.xml
  68. 342 0
      samples/boot/oauth2login/README.adoc
  69. 173 0
      samples/boot/oauth2login/pom.xml
  70. 15 0
      samples/boot/oauth2login/spring-security-samples-boot-oauth2login.gradle
  71. 405 0
      samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java
  72. 129 0
      samples/boot/oauth2login/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ClientRegistrationAutoConfiguration.java
  73. 108 0
      samples/boot/oauth2login/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2LoginAutoConfiguration.java
  74. 34 0
      samples/boot/oauth2login/src/main/java/org/springframework/security/samples/OAuth2LoginApplication.java
  75. 85 0
      samples/boot/oauth2login/src/main/java/org/springframework/security/samples/user/GitHubOAuth2User.java
  76. 36 0
      samples/boot/oauth2login/src/main/java/org/springframework/security/samples/web/MainController.java
  77. 44 0
      samples/boot/oauth2login/src/main/resources/META-INF/oauth2-clients-defaults.yml
  78. 4 0
      samples/boot/oauth2login/src/main/resources/META-INF/spring.factories
  79. 34 0
      samples/boot/oauth2login/src/main/resources/application.yml
  80. 33 0
      samples/boot/oauth2login/src/main/resources/templates/index.html
  81. 37 13
      web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java

+ 7 - 0
config/pom.xml

@@ -113,6 +113,13 @@
       <scope>compile</scope>
       <optional>true</optional>
     </dependency>
+    <dependency>
+      <groupId>org.springframework.security</groupId>
+      <artifactId>spring-security-oauth2-client</artifactId>
+      <version>5.0.0.BUILD-SNAPSHOT</version>
+      <scope>compile</scope>
+      <optional>true</optional>
+    </dependency>
     <dependency>
       <groupId>org.springframework.security</groupId>
       <artifactId>spring-security-openid</artifactId>

+ 1 - 0
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'

+ 8 - 0
config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java

@@ -77,6 +77,10 @@ final class FilterComparator implements Comparator<Filter>, 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<Filter>, 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);

+ 153 - 0
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<HttpSecurity>());
 	}
 
+	/**
+	 * Configures authentication against an external <i>OAuth 2.0</i> or <i>OpenID Connect 1.0</i> Provider.
+	 * <br>
+	 * <br>
+	 *
+	 * The <i>&quot;authentication flow&quot;</i> is realized using the <b>Authorization Code Grant</b>,
+	 * as specified in the <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">OAuth 2.0 Authorization Framework</a>.
+	 * <br>
+	 * <br>
+	 *
+	 * As a prerequisite to using this feature, the developer must register a <i>Client</i> with an <i>Authorization Server</i>.
+	 * The output of the <i>Client Registration</i> 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 <i>Client</i> include: <i>client_id</i>, <i>client_secret</i>, <i>scope</i>, <i>redirect_uri</i>, etc.
+	 * There are also properties specific to the <i>Provider</i>, for example,
+	 * <i>Authorization Endpoint URI</i>, <i>Token Endpoint URI</i>, <i>UserInfo Endpoint URI</i>, etc.
+	 * <br>
+	 * <br>
+	 *
+	 * Multiple client support is provided for use cases where the application provides the user the option
+	 * for <i>&quot;Logging in&quot;</i> against one or more Providers, for example, <i>Google</i>, <i>GitHub</i>, <i>Facebook</i>, etc.
+	 * <br>
+	 * <br>
+	 *
+	 * {@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 <b>required</b>
+	 * and may be supplied via the {@link ApplicationContext} or configured using
+	 * {@link OAuth2LoginConfigurer#clients(org.springframework.security.oauth2.client.registration.ClientRegistrationRepository)}.
+	 * <br>
+	 * <br>
+	 *
+	 * The default configuration provides an auto-generated login page at <code>&quot;/login&quot;</code> and
+	 * redirects to <code>&quot;/login?error&quot;</code> 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 <code>&quot;/oauth2/authorization/code/{clientAlias}&quot;</code>.
+	 * Clicking through the link will initiate the <i>&quot;Authorization Request&quot;</i> flow
+	 * redirecting the end-user's user-agent to the <i>Authorization Endpoint</i> of the <i>Provider</i>.
+	 * Assuming the <i>Resource Owner</i> (end-user) grants the <i>Client</i> access, the <i>Authorization Server</i>
+	 * will redirect the end-user's user-agent to the <i>Redirection Endpoint</i> containing the <i>Authorization Code</i>
+	 * - the <i>Redirection Endpoint</i> is automatically configured for the application and
+	 * defaults to <code>&quot;/oauth2/authorize/code/{clientAlias}&quot;</code>.
+	 *
+	 * <p>
+	 * At this point in the <i>&quot;authentication flow&quot;</i>, the configured
+	 * {@link org.springframework.security.oauth2.client.authentication.AuthorizationGrantTokenExchanger}
+	 * will exchange the <i>Authorization Code</i> for an <i>Access Token</i> and then use it to access the protected resource
+	 * at the <i>UserInfo Endpoint</i> (via {@link org.springframework.security.oauth2.client.user.OAuth2UserService})
+	 * in order to retrieve the details of the <i>Resource Owner</i> (end-user) and establish the <i>&quot;authenticated&quot;</i> session.
+	 *
+	 * <h2>Example Configurations</h2>
+	 *
+	 * The minimal configuration defaults to automatically generating a login page at <code>&quot;/login&quot;</code>
+	 * and redirecting to <code>&quot;/login?error&quot;</code> when an authentication error occurs or redirecting to
+	 * <code>&quot;/&quot;</code> when an authenticated session is established.
+	 *
+	 * <pre>
+	 * &#064;EnableWebSecurity
+	 * public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	&#064;Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 *		http
+	 * 			.authorizeRequests()
+	 * 				.anyRequest().authenticated()
+	 * 				.and()
+	 * 			.oauth2Login();
+	 * 	}
+	 *
+	 *	&#064;Bean
+	 *	public ClientRegistrationRepository clientRegistrationRepository() {
+	 *		// ClientRegistrationRepositoryImpl must be composed of at least one ClientRegistration instance
+	 *		return new ClientRegistrationRepositoryImpl();
+	 *	}
+	 * }
+	 * </pre>
+	 *
+	 * The following shows the configuration options available for customizing the defaults.
+	 *
+	 * <pre>
+	 * &#064;EnableWebSecurity
+	 * public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	&#064;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"));
+	 * 	}
+	 *
+	 *	&#064;Bean
+	 *	public ClientRegistrationRepository clientRegistrationRepository() {
+	 *		// ClientRegistrationRepositoryImpl must be composed of at least one ClientRegistration instance
+	 *		return new ClientRegistrationRepositoryImpl();
+	 *	}
+	 *
+	 * 	&#064;Bean
+	 * 	public AuthorizationRequestUriBuilder authorizationRequestBuilder() {
+	 * 		// Custom URI builder for the &quot;Authorization Request&quot;
+	 * 		return new AuthorizationRequestUriBuilderImpl();
+	 * 	}
+	 *
+	 * 	&#064;Bean
+	 * 	public AuthorizationGrantTokenExchanger&lt;AuthorizationCodeAuthenticationToken&gt; authorizationCodeTokenExchanger() {
+	 * 		// Custom implementation that exchanges an &quot;Authorization Code Grant&quot; for an &quot;Access Token&quot;
+	 * 		return new AuthorizationCodeTokenExchangerImpl();
+	 * 	}
+	 *
+	 * 	&#064;Bean
+	 * 	public OAuth2UserService userInfoService() {
+	 * 		// Custom implementation that retrieves the details of the authenticated user at the &quot;UserInfo Endpoint&quot;
+	 * 		return new OAuth2UserServiceImpl();
+	 * 	}
+	 *
+	 * 	&#064;Bean
+	 * 	public Converter&lt;ClientHttpResponse, UserInfo&gt; userInfoConverter() {
+	 * 		// Default converter implementation for UserInfo
+	 * 		return new org.springframework.security.oauth2.client.user.converter.UserInfoConverter();
+	 * 	}
+	 * }
+	 * </pre>
+	 *
+	 * @author Joe Grandja
+	 * @since 5.0
+	 * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant Flow</a>
+	 * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.1">Section 4.1.1 Authorization Request</a>
+	 * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.2">Section 4.1.2 Authorization Response</a>
+	 * @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<HttpSecurity> oauth2Login() throws Exception {
+		return getOrApply(new OAuth2LoginConfigurer<HttpSecurity>());
+	}
+
 	/**
 	 * Configures channel security. In order for this configuration to be useful at least
 	 * one mapping to a required channel must be provided.

+ 123 - 0
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<H extends HttpSecurityBuilder<H>> extends
+		AbstractAuthenticationFilterConfigurer<H, AuthorizationCodeAuthenticationFilterConfigurer<H>, AuthorizationCodeAuthenticationProcessingFilter> {
+
+	private AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> authorizationCodeTokenExchanger;
+	private OAuth2UserService userInfoService;
+	private Map<URI, Converter<ClientHttpResponse, ? extends OAuth2User>> userInfoTypeConverters = new HashMap<>();
+
+
+	AuthorizationCodeAuthenticationFilterConfigurer() {
+		super(new AuthorizationCodeAuthenticationProcessingFilter(), null);
+	}
+
+	AuthorizationCodeAuthenticationFilterConfigurer<H> 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<H> authorizationCodeTokenExchanger(
+			AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> authorizationCodeTokenExchanger) {
+
+		Assert.notNull(authorizationCodeTokenExchanger, "authorizationCodeTokenExchanger cannot be null");
+		this.authorizationCodeTokenExchanger = authorizationCodeTokenExchanger;
+		return this;
+	}
+
+	AuthorizationCodeAuthenticationFilterConfigurer<H> userInfoService(OAuth2UserService userInfoService) {
+		Assert.notNull(userInfoService, "userInfoService cannot be null");
+		this.userInfoService = userInfoService;
+		return this;
+	}
+
+	AuthorizationCodeAuthenticationFilterConfigurer<H> userInfoTypeConverter(Converter<ClientHttpResponse, ? extends OAuth2User> 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<AuthorizationCodeAuthenticationToken> 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;
+	}
+}

+ 61 - 0
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<B extends HttpSecurityBuilder<B>> extends
+		AbstractHttpConfigurer<AuthorizationCodeRequestRedirectFilterConfigurer<B>, B> {
+
+	private AuthorizationRequestUriBuilder authorizationRequestBuilder;
+
+	AuthorizationCodeRequestRedirectFilterConfigurer<B> 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<B> 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;
+	}
+}

+ 154 - 0
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<B extends HttpSecurityBuilder<B>> extends
+		AbstractHttpConfigurer<OAuth2LoginConfigurer<B>, B> {
+
+	private final AuthorizationCodeRequestRedirectFilterConfigurer<B> authorizationCodeRequestRedirectFilterConfigurer;
+	private final AuthorizationCodeAuthenticationFilterConfigurer<B> authorizationCodeAuthenticationFilterConfigurer;
+	private final UserInfoEndpointConfig userInfoEndpointConfig;
+
+	public OAuth2LoginConfigurer() {
+		this.authorizationCodeRequestRedirectFilterConfigurer = new AuthorizationCodeRequestRedirectFilterConfigurer<>();
+		this.authorizationCodeAuthenticationFilterConfigurer = new AuthorizationCodeAuthenticationFilterConfigurer<>();
+		this.userInfoEndpointConfig = new UserInfoEndpointConfig();
+	}
+
+	public OAuth2LoginConfigurer<B> clients(ClientRegistration... clientRegistrations) {
+		Assert.notEmpty(clientRegistrations, "clientRegistrations cannot be empty");
+		return clients(new InMemoryClientRegistrationRepository(Arrays.asList(clientRegistrations)));
+	}
+
+	public OAuth2LoginConfigurer<B> 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<B> authorizationRequestBuilder(AuthorizationRequestUriBuilder authorizationRequestBuilder) {
+		Assert.notNull(authorizationRequestBuilder, "authorizationRequestBuilder cannot be null");
+		this.authorizationCodeRequestRedirectFilterConfigurer.authorizationRequestBuilder(authorizationRequestBuilder);
+		return this;
+	}
+
+	public OAuth2LoginConfigurer<B> authorizationCodeTokenExchanger(
+			AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> 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<B> userInfoService(OAuth2UserService userInfoService) {
+			Assert.notNull(userInfoService, "userInfoService cannot be null");
+			OAuth2LoginConfigurer.this.authorizationCodeAuthenticationFilterConfigurer.userInfoService(userInfoService);
+			return this.and();
+		}
+
+		public OAuth2LoginConfigurer<B> userInfoTypeConverter(Converter<ClientHttpResponse, ? extends OAuth2User> 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<B> 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 <B extends HttpSecurityBuilder<B>> 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 <B extends HttpSecurityBuilder<B>> 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<String, String> 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());
+			}
+		}
+	}
+}

+ 2 - 2
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() == """<html><head><title>Login Page</title></head><body onload='document.f.username.focus();'>
-<p><font color='red'>Your login attempt was not successful, try again.<br/><br/>Reason: Bad credentials</font></p><h3>Login with Username and Password</h3><form name='f' action='/login' method='POST'>
+<p style='color:red;'>Your login attempt was not successful, try again.<br/><br/>Reason: Bad credentials</p><h3>Login with Username and Password</h3><form name='f' action='/login' method='POST'>
 <table>
 	<tr><td>User:</td><td><input type='text' name='username' value=''></td></tr>
 	<tr><td>Password:</td><td><input type='password' name='password'/></td></tr>
@@ -107,7 +107,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
 			springSecurityFilterChain.doFilter(request,response,chain)
 		then: "sent to default success page"
 			response.getContentAsString() == """<html><head><title>Login Page</title></head><body onload='document.f.username.focus();'>
-<p><font color='green'>You have been logged out</font></p><h3>Login with Username and Password</h3><form name='f' action='/login' method='POST'>
+<p style='color:green;'>You have been logged out</p><h3>Login with Username and Password</h3><form name='f' action='/login' method='POST'>
 <table>
 	<tr><td>User:</td><td><input type='text' name='username' value=''></td></tr>
 	<tr><td>Password:</td><td><input type='password' name='password'/></td></tr>

+ 8 - 35
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] - <<jackson,Jackson Support>>
-* https://github.com/spring-projects/spring-security/pull/4116[#4116] - <<headers-referrer,Referrer Policy>>
-* https://github.com/spring-projects/spring-security/pull/3938[#3938] - Add <<request-matching,HTTP response splitting prevention>>
-* https://github.com/spring-projects/spring-security/issues/3949[#3949] - Add <<mvc-authentication-principal,bean reference support to @AuthenticationPrincipal>>.
-* 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 <<appendix-proxy-server,Proxy Server>> 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 <<jc-custom-dsls>>
-* https://github.com/spring-projects/spring-security/issues/3899[#3899] - <<nsa-concurrency-control-max-sessions,concurrency-control@max-sessions>> supports unlimited sessions.
-* https://github.com/spring-projects/spring-security/issues/4097[#4097] - <<nsa-intercept-url-request-matcher-ref,intercept-url@request-matcher-ref>> 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)

+ 156 - 0
oauth2/oauth2-client/pom.xml

@@ -0,0 +1,156 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>org.springframework.security</groupId>
+  <artifactId>spring-security-oauth2-client</artifactId>
+  <version>5.0.0.BUILD-SNAPSHOT</version>
+  <name>spring-security-oauth2-client</name>
+  <description>spring-security-oauth2-client</description>
+  <url>http://spring.io/spring-security</url>
+  <organization>
+    <name>spring.io</name>
+    <url>http://spring.io/</url>
+  </organization>
+  <licenses>
+    <license>
+      <name>The Apache Software License, Version 2.0</name>
+      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+      <distribution>repo</distribution>
+    </license>
+  </licenses>
+  <developers>
+    <developer>
+      <id>rwinch</id>
+      <name>Rob Winch</name>
+      <email>rwinch@pivotal.io</email>
+    </developer>
+    <developer>
+      <id>jgrandja</id>
+      <name>Joe Grandja</name>
+      <email>jgrandja@pivotal.io</email>
+    </developer>
+  </developers>
+  <scm>
+    <connection>scm:git:git://github.com/spring-projects/spring-security</connection>
+    <developerConnection>scm:git:git://github.com/spring-projects/spring-security</developerConnection>
+    <url>https://github.com/spring-projects/spring-security</url>
+  </scm>
+  <dependencyManagement>
+    <dependencies>
+      <dependency>
+        <groupId>org.springframework</groupId>
+        <artifactId>spring-framework-bom</artifactId>
+        <version>4.3.5.RELEASE</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
+    </dependencies>
+  </dependencyManagement>
+  <dependencies>
+    <dependency>
+      <groupId>com.nimbusds</groupId>
+      <artifactId>oauth2-oidc-sdk</artifactId>
+      <version>5.21</version>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework.security</groupId>
+      <artifactId>spring-security-core</artifactId>
+      <version>5.0.0.BUILD-SNAPSHOT</version>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework.security</groupId>
+      <artifactId>spring-security-oauth2-core</artifactId>
+      <version>5.0.0.BUILD-SNAPSHOT</version>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework.security</groupId>
+      <artifactId>spring-security-web</artifactId>
+      <version>5.0.0.BUILD-SNAPSHOT</version>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework</groupId>
+      <artifactId>spring-core</artifactId>
+      <scope>compile</scope>
+      <exclusions>
+        <exclusion>
+          <artifactId>commons-logging</artifactId>
+          <groupId>commons-logging</groupId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework</groupId>
+      <artifactId>spring-web</artifactId>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>commons-logging</groupId>
+      <artifactId>commons-logging</artifactId>
+      <version>1.2</version>
+      <scope>compile</scope>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>javax.servlet</groupId>
+      <artifactId>javax.servlet-api</artifactId>
+      <version>3.1.0</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>ch.qos.logback</groupId>
+      <artifactId>logback-classic</artifactId>
+      <version>1.1.2</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.12</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.assertj</groupId>
+      <artifactId>assertj-core</artifactId>
+      <version>3.6.2</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <version>1.10.19</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>jcl-over-slf4j</artifactId>
+      <version>1.7.7</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework</groupId>
+      <artifactId>spring-test</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+  <repositories>
+    <repository>
+      <id>spring-snapshot</id>
+      <url>https://repo.spring.io/snapshot</url>
+    </repository>
+  </repositories>
+  <build>
+    <plugins>
+      <plugin>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <configuration>
+          <source>1.8</source>
+          <target>1.8</target>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+</project>

+ 12 - 0
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'
+}

+ 225 - 0
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 <i>OAuth 2.0 Authorization Response</i> for the authorization code grant flow.
+ *
+ * <p>
+ * This <code>Filter</code> processes the <i>Authorization Response</i> in the following step sequence:
+ *
+ * <ol>
+ * <li>
+ *	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 <i>Authorization Request</i>) parameters
+ *	to the {@link OAuth2Parameter#REDIRECT_URI} (provided in the <i>Authorization Request</i>)
+ *	and redirect the end-user's user-agent back to this <code>Filter</code> (the client).
+ * </li>
+ * <li>
+ *  This <code>Filter</code> 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 <i>Token Endpoint</i> for exchanging the {@link OAuth2Parameter#CODE} for an {@link AccessToken}.
+ * </li>
+ * <li>
+ *  Upon receiving the <i>Access Token Request</i>, the authorization server will authenticate the client,
+ *  verify the {@link OAuth2Parameter#CODE}, and ensure that the {@link OAuth2Parameter#REDIRECT_URI}
+ *	received matches the <code>URI</code> originally provided in the <i>Authorization Request</i>.
+ *	If the request is valid, the authorization server will respond back with a {@link TokenResponseAttributes}.
+ * </li>
+ * <li>
+ *  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 <i>UserInfo Endpoint</i> (using the {@link AccessToken})
+ *  to obtain the end-user's (resource owner) attributes and return it in the form of an {@link OAuth2User}.
+ * </li>
+ * <li>
+ *  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 <code>Filter</code> at which point the session is considered <i>&quot;authenticated&quot;</i>.
+ * </li>
+ * </ol>
+ *
+ * <p>
+ * <b>NOTE:</b> Steps 4-5 are <b>not</b> part of the authorization code grant flow and instead are
+ * <i>&quot;authentication flow&quot;</i> 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 <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant Flow</a>
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.2">Section 4.1.2 Authorization Response</a>
+ */
+public class 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());
+		}
+	}
+}

+ 120 - 0
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 <i>authorization code</i> credential with the authorization server's <i>Token Endpoint</i>
+ * and if valid, exchanging it for an <i>access token</i> credential.
+ * Additionally, it will also obtain the end-user's (resource owner) attributes from the <i>UserInfo Endpoint</i>
+ * (using the <i>access token</i>) and create a <code>Principal</code> in the form of an {@link OAuth2User}
+ * associating it with the returned {@link OAuth2AuthenticationToken}.
+ *
+ * <p>
+ * The {@link AuthorizationCodeAuthenticationProvider} uses an {@link AuthorizationGrantTokenExchanger}
+ * to make a request to the authorization server's <i>Token Endpoint</i>
+ * to verify the {@link AuthorizationCodeAuthenticationToken#getAuthorizationCode()}.
+ * If the request is valid, the authorization server will respond back with a {@link TokenResponseAttributes}.
+ *
+ * <p>
+ * 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}.
+ *
+ * <p>
+ * 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 <i>&quot;authenticated&quot;</i>.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see AuthorizationCodeAuthenticationToken
+ * @see AuthorizationGrantTokenExchanger
+ * @see TokenResponseAttributes
+ * @see AccessToken
+ * @see OAuth2UserService
+ * @see OAuth2User
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant Flow</a>
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request</a>
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response</a>
+ */
+public class AuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
+	private final AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> authorizationCodeTokenExchanger;
+	private final OAuth2UserService userInfoService;
+	private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
+
+	public AuthorizationCodeAuthenticationProvider(
+			AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> 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<? extends GrantedAuthority> 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;
+	}
+}

+ 63 - 0
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 <i>authorization code grant</i> credential for a specific client identified in {@link #getClientRegistration()}.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see AuthorizationGrantAuthenticationToken
+ * @see ClientRegistration
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3.1">Section 1.3.1 Authorization Code Grant</a>
+ */
+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;
+	}
+}

+ 159 - 0
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 <code>Filter</code> initiates the authorization code grant flow by redirecting
+ * the end-user's user-agent to the authorization server's <i>Authorization Endpoint</i>.
+ *
+ * <p>
+ * It uses an {@link AuthorizationRequestUriBuilder} to build the <i>OAuth 2.0 Authorization Request</i>,
+ * which is used as the redirect <code>URI</code> to the <i>Authorization Endpoint</i>.
+ * The redirect <code>URI</code> 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 <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant Flow</a>
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.1">Section 4.1.1 Authorization Request</a>
+ */
+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();
+	}
+}

+ 50 - 0
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 <i>authorization grant</i> credential for a specific {@link AuthorizationGrantType}.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see AuthorizationGrantType
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3">Section 1.3 Authorization Grant</a>
+ */
+public abstract class AuthorizationGrantAuthenticationToken extends AbstractAuthenticationToken {
+	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
+	private final AuthorizationGrantType authorizationGrantType;
+
+	protected AuthorizationGrantAuthenticationToken(AuthorizationGrantType authorizationGrantType,
+													Collection<? extends GrantedAuthority> authorities) {
+
+		super(authorities);
+		Assert.notNull(authorizationGrantType, "authorizationGrantType cannot be null");
+		this.authorizationGrantType = authorizationGrantType;
+	}
+
+	public AuthorizationGrantType getGrantType() {
+		return this.authorizationGrantType;
+	}
+}

+ 40 - 0
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 <i>&quot;exchanging&quot;</i>
+ * an <i>authorization grant</i> credential (for example, an authorization code) for an
+ * <i>access token</i> credential at the authorization server's <i>Token Endpoint</i>.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see AuthorizationGrantType
+ * @see AuthorizationGrantAuthenticationToken
+ * @see TokenResponseAttributes
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3">Section 1.3 Authorization Grant</a>
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request (Authorization Code Grant)</a>
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response (Authorization Code Grant)</a>
+ */
+public interface AuthorizationGrantTokenExchanger<T extends AuthorizationGrantAuthenticationToken>  {
+
+	TokenResponseAttributes exchange(T authorizationGrantAuthentication) throws OAuth2AuthenticationException;
+
+}

+ 45 - 0
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.
+ *
+ * <p>
+ * Used by the {@link AuthorizationCodeRequestRedirectFilter} for persisting the <i>Authorization Request</i>
+ * before it initiates the authorization code grant flow.
+ * As well, used by the {@link AuthorizationCodeAuthenticationProcessingFilter} when resolving
+ * the associated <i>Authorization Request</i> during the handling of the <i>Authorization Response</i>.
+ *
+ * @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);
+
+}

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

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

+ 54 - 0
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 <i>Authorization Request</i> and correlated in the <i>Authorization Response</i> (or <i>Error Response</i>).
+ *
+ * <p>
+ * <b>NOTE:</b> The value of the <i>state</i> parameter is an opaque <code>String</code>
+ * used by the client to prevent cross-site request forgery, as described in
+ * <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-10.12">Section 10.12</a> 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()));
+	}
+}

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

+ 66 - 0
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 <i>OAuth 2.0</i> related {@link Authentication} errors.
+ *
+ * <p>
+ * There are a number of scenarios where an error may occur, for example:
+ * <ul>
+ *  <li>The authorization request or token request is missing a required parameter</li>
+ *	<li>Missing or invalid client identifier</li>
+ *	<li>Invalid or mismatching redirection URI</li>
+ *	<li>The requested scope is invalid, unknown, or malformed</li>
+ *	<li>The resource owner or authorization server denied the access request</li>
+ *	<li>Client authentication failed</li>
+ *	<li>The provided authorization grant (authorization code, resource owner credentials) is invalid, expired, or revoked</li>
+ * </ul>
+ *
+ * @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;
+	}
+}

+ 87 - 0
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 <i>OAuth 2.0</i> {@link Authentication}.
+ *
+ * <p>
+ * It associates an {@link OAuth2User}, {@link ClientRegistration} and an {@link AccessToken}.
+ * This <code>Authentication</code> is considered <i>&quot;authenticated&quot;</i> 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 <i>UserInfo Endpoint</i>.
+ *
+ * @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<? extends GrantedAuthority> 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;
+	}
+}

+ 142 - 0
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 <i>&quot;exchanges&quot;</i>
+ * an <i>authorization code</i> credential for an <i>access token</i> credential
+ * at the authorization server's <i>Token Endpoint</i>.
+ *
+ * <p>
+ * <b>NOTE:</b> This implementation uses the <b>Nimbus OAuth 2.0 SDK</b> internally.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see AuthorizationCodeAuthenticationToken
+ * @see TokenResponseAttributes
+ * @see <a target="_blank" href="https://connect2id.com/products/nimbus-oauth-openid-connect-sdk">Nimbus OAuth 2.0 SDK</a>
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request (Authorization Code Grant)</a>
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response (Authorization Code Grant)</a>
+ */
+public class NimbusAuthorizationCodeTokenExchanger implements AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> {
+	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<String> scopes = Collections.emptySet();
+		if (!CollectionUtils.isEmpty(accessTokenResponse.getTokens().getAccessToken().getScope())) {
+			scopes = new HashSet<>(accessTokenResponse.getTokens().getAccessToken().getScope().toStringList());
+		}
+		Map<String, Object> 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);
+		}
+	}
+}

+ 279 - 0
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<String> 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<String> getScopes() {
+		return this.scopes;
+	}
+
+	protected void setScopes(Set<String> 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<String> 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");
+		}
+	}
+}

+ 129 - 0
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<String> 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<String> getScopes() {
+		return this.scopes;
+	}
+
+	public void setScopes(Set<String> 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;
+	}
+}

+ 33 - 0
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<ClientRegistration> getRegistrations();
+
+}

+ 59 - 0
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<ClientRegistration> clientRegistrations;
+
+	public InMemoryClientRegistrationRepository(List<ClientRegistration> clientRegistrations) {
+		Assert.notEmpty(clientRegistrations, "clientRegistrations cannot be empty");
+		this.clientRegistrations = Collections.unmodifiableList(clientRegistrations);
+	}
+
+	@Override
+	public ClientRegistration getRegistrationByClientId(String clientId) {
+		Optional<ClientRegistration> 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> clientRegistration =
+				this.clientRegistrations.stream()
+						.filter(c -> c.getClientAlias().equals(clientAlias))
+						.findFirst();
+		return clientRegistration.isPresent() ? clientRegistration.get() : null;
+	}
+
+	@Override
+	public List<ClientRegistration> getRegistrations() {
+		return this.clientRegistrations;
+	}
+}

+ 42 - 0
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 <i>UserInfo Endpoint</i>
+ * using the provided {@link OAuth2AuthenticationToken#getAccessToken()}
+ * and returning an {@link AuthenticatedPrincipal} in the form of an {@link OAuth2User}
+ * (for a standard <i>OAuth 2.0 Provider</i>) or {@link UserInfo} (for an <i>OpenID Connect 1.0 Provider</i>).
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see OAuth2AuthenticationToken
+ * @see AuthenticatedPrincipal
+ * @see OAuth2User
+ * @see UserInfo
+ */
+public interface OAuth2UserService {
+
+	OAuth2User loadUser(OAuth2AuthenticationToken token) throws OAuth2AuthenticationException;
+
+}

+ 53 - 0
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<T extends OAuth2User> implements Converter<ClientHttpResponse, T> {
+	private final HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
+
+	protected AbstractOAuth2UserConverter() {
+	}
+
+	@Override
+	public final T convert(ClientHttpResponse clientHttpResponse) {
+		Map<String, Object> userAttributes;
+
+		try {
+			userAttributes = (Map<String, Object>) 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<String, Object> userAttributes);
+
+}

+ 51 - 0
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<T extends OAuth2User> implements Converter<ClientHttpResponse, T> {
+	private final HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
+	private final Class<T> customType;
+
+	public CustomOAuth2UserConverter(Class<T> 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;
+	}
+}

+ 41 - 0
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<OAuth2User> {
+	private final String nameAttributeKey;
+
+	public OAuth2UserConverter(String nameAttributeKey) {
+		Assert.hasText(nameAttributeKey, "nameAttributeKey cannot be empty");
+		this.nameAttributeKey = nameAttributeKey;
+	}
+
+	@Override
+	protected OAuth2User convert(Map<String, Object> userAttributes) {
+		return new DefaultOAuth2User(userAttributes, this.nameAttributeKey);
+	}
+}

+ 34 - 0
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<UserInfo> {
+
+	@Override
+	protected UserInfo convert(Map<String, Object> userAttributes) {
+		return new DefaultUserInfo(userAttributes);
+	}
+}

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

+ 121 - 0
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 <b>Nimbus OAuth 2.0 SDK</b> internally.
+ *
+ * <p>
+ * This implementation uses a <code>Map</code> of <code>Converter</code>'s <i>keyed</i> by <code>URI</code>.
+ * The <code>URI</code> represents the <i>UserInfo Endpoint</i> address and the mapped <code>Converter</code>
+ * is capable of converting the <i>UserInfo Response</i> to either an
+ * {@link OAuth2User} (for a standard <i>OAuth 2.0 Provider</i>) or
+ * {@link UserInfo} (for an <i>OpenID Connect 1.0 Provider</i>).
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see OAuth2AuthenticationToken
+ * @see AuthenticatedPrincipal
+ * @see OAuth2User
+ * @see UserInfo
+ * @see Converter
+ * @see <a target="_blank" href="https://connect2id.com/products/nimbus-oauth-openid-connect-sdk">Nimbus OAuth 2.0 SDK</a>
+ */
+public class NimbusOAuth2UserService implements OAuth2UserService {
+	private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";
+	private final Map<URI, Converter<ClientHttpResponse, ? extends OAuth2User>> userInfoTypeConverters;
+
+	public NimbusOAuth2UserService(Map<URI, Converter<ClientHttpResponse, ? extends OAuth2User>> 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<ClientHttpResponse, ? extends OAuth2User> 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;
+	}
+}

+ 45 - 0
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<HttpServletRequest, AuthorizationCodeAuthorizationResponseAttributes> {
+
+	@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;
+	}
+}

+ 53 - 0
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<HttpServletRequest, ErrorResponseAttributes> {
+
+	@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;
+	}
+}

+ 254 - 0
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<Authentication> 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<AuthenticationException> 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;
+	}
+}

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

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

+ 138 - 0
oauth2/oauth2-core/pom.xml

@@ -0,0 +1,138 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>org.springframework.security</groupId>
+  <artifactId>spring-security-oauth2-core</artifactId>
+  <version>5.0.0.BUILD-SNAPSHOT</version>
+  <name>spring-security-oauth2-core</name>
+  <description>spring-security-oauth2-core</description>
+  <url>http://spring.io/spring-security</url>
+  <organization>
+    <name>spring.io</name>
+    <url>http://spring.io/</url>
+  </organization>
+  <licenses>
+    <license>
+      <name>The Apache Software License, Version 2.0</name>
+      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+      <distribution>repo</distribution>
+    </license>
+  </licenses>
+  <developers>
+    <developer>
+      <id>rwinch</id>
+      <name>Rob Winch</name>
+      <email>rwinch@pivotal.io</email>
+    </developer>
+    <developer>
+      <id>jgrandja</id>
+      <name>Joe Grandja</name>
+      <email>jgrandja@pivotal.io</email>
+    </developer>
+  </developers>
+  <scm>
+    <connection>scm:git:git://github.com/spring-projects/spring-security</connection>
+    <developerConnection>scm:git:git://github.com/spring-projects/spring-security</developerConnection>
+    <url>https://github.com/spring-projects/spring-security</url>
+  </scm>
+  <dependencyManagement>
+    <dependencies>
+      <dependency>
+        <groupId>org.springframework</groupId>
+        <artifactId>spring-framework-bom</artifactId>
+        <version>4.3.5.RELEASE</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
+    </dependencies>
+  </dependencyManagement>
+  <dependencies>
+    <dependency>
+      <groupId>org.springframework.security</groupId>
+      <artifactId>spring-security-core</artifactId>
+      <version>5.0.0.BUILD-SNAPSHOT</version>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework</groupId>
+      <artifactId>spring-core</artifactId>
+      <scope>compile</scope>
+      <exclusions>
+        <exclusion>
+          <artifactId>commons-logging</artifactId>
+          <groupId>commons-logging</groupId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework</groupId>
+      <artifactId>spring-web</artifactId>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>commons-logging</groupId>
+      <artifactId>commons-logging</artifactId>
+      <version>1.2</version>
+      <scope>compile</scope>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>javax.servlet</groupId>
+      <artifactId>javax.servlet-api</artifactId>
+      <version>3.1.0</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>ch.qos.logback</groupId>
+      <artifactId>logback-classic</artifactId>
+      <version>1.1.2</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.12</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.assertj</groupId>
+      <artifactId>assertj-core</artifactId>
+      <version>3.6.2</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <version>1.10.19</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>jcl-over-slf4j</artifactId>
+      <version>1.7.7</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework</groupId>
+      <artifactId>spring-test</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+  <repositories>
+    <repository>
+      <id>spring-snapshot</id>
+      <url>https://repo.spring.io/snapshot</url>
+    </repository>
+  </repositories>
+  <build>
+    <plugins>
+      <plugin>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <configuration>
+          <source>1.8</source>
+          <target>1.8</target>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+</project>

+ 9 - 0
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'
+}

+ 59 - 0
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 <i>Security Token</i> implementations.
+ *
+ * <p>
+ * 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;
+	}
+}

+ 88 - 0
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 <i>OAuth 2.0 Access Token</i>.
+ *
+ * <p>
+ * 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 <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.4">Section 1.4 Access Token</a>
+ */
+public class AccessToken extends AbstractToken {
+	private final TokenType tokenType;
+	private final Set<String> scopes;
+	private final Map<String,Object> 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<String> scopes) {
+		this(tokenType, tokenValue, issuedAt, expiresAt, scopes, Collections.emptyMap());
+	}
+
+	public AccessToken(TokenType tokenType, String tokenValue, Instant issuedAt, Instant expiresAt,
+						Set<String> scopes, Map<String,Object> 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<String> getScopes() {
+		return this.scopes;
+	}
+
+	public Map<String, Object> getAdditionalParameters() {
+		return additionalParameters;
+	}
+}

+ 46 - 0
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.
+ *
+ * <p>
+ * The <i>OAuth 2.0 Authorization Framework</i> 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.
+ *
+ * <p>
+ * <b>NOTE:</b> &quot;authorization code&quot; is currently the only supported grant type.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3">Section 1.3 Authorization Grant</a>
+ */
+public enum AuthorizationGrantType {
+	AUTHORIZATION_CODE("authorization_code");
+
+	private final String value;
+
+	AuthorizationGrantType(String value) {
+		this.value = value;
+	}
+
+	public String value() {
+		return this.value;
+	}
+}

+ 38 - 0
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 <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-2.3">Section 2.3 Client Authentication</a>
+ */
+public enum ClientAuthenticationMethod {
+	HEADER("header"),
+	FORM("form");
+
+	private final String value;
+
+	ClientAuthenticationMethod(String value) {
+		this.value = value;
+	}
+
+	public String value() {
+		return this.value;
+	}
+}

+ 74 - 0
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 <i>OAuth 2.0 Error</i>.
+ *
+ * <p>
+ * 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 <i>new</i> code defined in the <i>OAuth Extensions Error Registry</i>,
+ * for cases where protocol extensions require additional error code(s) above the standard codes.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-11.4">Section 11.4 OAuth Extensions Error Registry</a>
+ */
+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() : "");
+	}
+}

+ 44 - 0
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 <i>OAuth 2.0 Authorization Response</i> for the authorization code grant type.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.2">Section 4.1.2 Authorization Response</a>
+ */
+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;
+	}
+}

+ 76 - 0
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 <i>OAuth 2.0 Access Token Request</i> for the authorization code grant type.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request</a>
+ */
+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;
+		}
+	}
+}

+ 127 - 0
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 <i>OAuth 2.0 Authorization Request</i>
+ * for the authorization code grant type or implicit grant type.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see AuthorizationGrantType
+ * @see ResponseType
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.1">Section 4.1.1 Authorization Code Grant Request</a>
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.2.1">Section 4.2.1 Implicit Grant Request</a>
+ */
+public final class AuthorizationRequestAttributes implements Serializable {
+	private String authorizeUri;
+	private AuthorizationGrantType authorizationGrantType;
+	private ResponseType responseType;
+	private String clientId;
+	private String redirectUri;
+	private Set<String> 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<String> 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<String> 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;
+		}
+	}
+}

+ 96 - 0
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 <i>OAuth 2.0 Error Response</i>.
+ *
+ * <p>
+ * An error response may be returned from either of the following locations:
+ * <ul>
+ * <li><a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.2.1">Section 4.1.2.1</a> Authorization Code Grant Response</li>
+ * <li><a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.2.2.1">Section 4.2.2.1</a> Implicit Grant Response</li>
+ * <li><a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-5.2">Section 5.2</a> Access Token Response</li>
+ * <li><a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-7.2">Section 7.2</a> Protected Resource Response</li>
+ * </ul>
+ *
+ * @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;
+		}
+	}
+}

+ 46 - 0
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 <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-11.2">11.2 OAuth Parameters Registry</a>
+ */
+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";
+
+}

+ 46 - 0
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 <i>response_type</i> 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 <i>response_type</i> parameter with the desired grant type before initiating the authorization request.
+ *
+ * <p>
+ * The <i>response_type</i> parameter value may be one of &quot;code&quot; for requesting an authorization code or
+ * &quot;token&quot; for requesting an access token (implicit grant).
+
+ * <p>
+ * <b>NOTE:</b> &quot;code&quot; is currently the only supported response type.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-3.1.1">Section 3.1.1 Response Type</a>
+ */
+public enum ResponseType {
+	CODE("code");
+
+	private final String value;
+
+	ResponseType(String value) {
+		this.value = value;
+	}
+
+	public String value() {
+		return this.value;
+	}
+}

+ 109 - 0
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 <i>OAuth 2.0 Access Token Response</i>.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see AccessToken
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-5.1">Section 5.1 Access Token Response</a>
+ */
+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<String> getScopes() {
+		return this.accessToken.getScopes();
+	}
+
+	public Map<String, Object> 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<String> scopes;
+		private Map<String,Object> 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<String> scopes) {
+			this.scopes = scopes;
+			return this;
+		}
+
+		public Builder additionalParameters(Map<String,Object> 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;
+		}
+	}
+}

+ 21 - 0
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
+ * <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-3.1">Authorization Endpoint</a>
+ * and <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-3.2">Token Endpoint</a>.
+ */
+package org.springframework.security.oauth2.core.endpoint;

+ 19 - 0
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 <i>OAuth 2.0 Authorization Framework</i>.
+ */
+package org.springframework.security.oauth2.core;

+ 153 - 0
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}.
+ *
+ * <p>
+ * User attribute names are <b><i>not</i></b> standardized between providers
+ * and therefore it is required that the user supply the <i>key</i>
+ * for the user's &quot;name&quot; attribute to one of the constructors.
+ * The <i>key</i> will be used for accessing the &quot;name&quot; of the
+ * <code>Principal</code> (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<GrantedAuthority> authorities;
+	private final Map<String, Object> attributes;
+	private final String nameAttributeKey;
+
+	public DefaultOAuth2User(Map<String, Object> attributes, String nameAttributeKey) {
+		this(Collections.emptySet(), attributes, nameAttributeKey);
+	}
+
+	public DefaultOAuth2User(Set<GrantedAuthority> authorities, Map<String, Object> 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<? extends GrantedAuthority> getAuthorities() {
+		return this.authorities;
+	}
+
+	@Override
+	public Map<String, Object> 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<GrantedAuthority> sortAuthorities(Set<GrantedAuthority> authorities) {
+		if (CollectionUtils.isEmpty(authorities)) {
+			return Collections.emptySet();
+		}
+
+		SortedSet<GrantedAuthority> 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();
+	}
+}

+ 55 - 0
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 <code>Principal</code>
+ * that is registered with a standard <i>OAuth 2.0 Provider</i>.
+ *
+ * <p>
+ * 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 &quot;name&quot; and &quot;value&quot; and
+ * is keyed by the &quot;name&quot; in {@link #getAttributes()}.
+ *
+ * <p>
+ * <b>NOTE:</b> Attribute names are <b><i>not</i></b> standardized between providers and therefore will vary.
+ * Please consult the provider's API documentation for the set of supported user attribute names.
+ *
+ * <p>
+ * 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<? extends GrantedAuthority> getAuthorities();
+
+	Map<String, Object> getAttributes();
+}

+ 19 - 0
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 <i>OAuth 2.0</i> representation of a user <code>Principal</code>.
+ */
+package org.springframework.security.oauth2.core.user;

+ 69 - 0
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 <i>OpenID Connect Core 1.0</i> specification
+ * and returned in either the <i>UserInfo Response</i> or in the <i>ID Token</i>.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims">Standard Claims</a>
+ * @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse">UserInfo Response</a>
+ * @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#IDToken">ID Token</a>
+ */
+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";
+}

+ 19 - 0
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 <i>OpenID Connect Core 1.0</i>.
+ */
+package org.springframework.security.oauth2.oidc;

+ 154 - 0
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}.
+ *
+ * <p>
+ * The <i>key</i> used for accessing the &quot;name&quot; of the
+ * <code>Principal</code> (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<String, Object> attributes) {
+		this(Collections.emptySet(), attributes);
+	}
+
+	public DefaultUserInfo(Set<GrantedAuthority> authorities, Map<String, Object> 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);
+	}
+}

+ 103 - 0
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 <code>Principal</code>
+ * that is registered with an <i>OpenID Connect 1.0 Provider</i>.
+ *
+ * <p>
+ * The structure of the user <code>Principal</code> is defined by the
+ * <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#UserInfo">UserInfo Endpoint</a>,
+ * which is an <i>OAuth 2.0 Protected Resource</i> that returns a set of
+ * <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims">Claims</a>
+ * about the authenticated End-User.
+ *
+ * <p>
+ * 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 <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html">OpenID Connect Core 1.0</a>
+ * @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#UserInfo">UserInfo Endpoint</a>
+ * @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims">Standard Claims</a>
+ */
+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();
+	}
+}

+ 19 - 0
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 <i>OpenID Connect Core 1.0</i> representation of a user <code>Principal</code>.
+ */
+package org.springframework.security.oauth2.oidc.user;

+ 7 - 0
samples/boot/helloworld/pom.xml

@@ -44,6 +44,13 @@
         <type>pom</type>
         <scope>import</scope>
       </dependency>
+      <dependency>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-dependencies</artifactId>
+        <version>1.5.0.BUILD-SNAPSHOT</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
     </dependencies>
   </dependencyManagement>
   <dependencies>

+ 7 - 0
samples/boot/insecure/pom.xml

@@ -44,6 +44,13 @@
         <type>pom</type>
         <scope>import</scope>
       </dependency>
+      <dependency>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-dependencies</artifactId>
+        <version>1.5.0.BUILD-SNAPSHOT</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
     </dependencies>
   </dependencyManagement>
   <dependencies>

+ 342 - 0
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:
+
+* <<google-login, Google>>
+* <<github-login, GitHub>>
+* <<facebook-login, Facebook>>
+* <<okta-login, Okta>>
+
+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 <<github-login, GitHub Login>>.
+** *web*
+*** _MainController_ - the root controller that displays user information after a successful login.
+
+*org.springframework.boot.autoconfigure.security.oauth2.client*
+
+[circle]
+* <<client-registration-auto-configuration-class, _ClientRegistrationAutoConfiguration_>> - a Spring Boot auto-configuration class
+ that automatically registers a _ClientRegistrationRepository_ bean in the _ApplicationContext_.
+* <<oauth2-login-auto-configuration-class, _OAuth2LoginAutoConfiguration_>> - 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 <<oauth2-login-auto-configuration, OAuth 2.0 Login auto-configuration>> 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 <<google-login-register-credentials, previous step>>)_ 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 <<oauth2-client-properties, OAuth client properties>> 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 <<google-login-register-credentials, Register OAuth 2.0 credentials>>.
+
+[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 <<oauth2-client-properties, OAuth client properties>>.
+====
+
+[[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 <<google-login-register-credentials, Register OAuth 2.0 credentials>>.
+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-default-client-properties, Default client property values>>.
+
+[[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, <<client-registration-auto-configuration-class, `ClientRegistrationAutoConfiguration`>> 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<ClientRegistration> 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();
+	}
+}
+----

+ 173 - 0
samples/boot/oauth2login/pom.xml

@@ -0,0 +1,173 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>org.springframework.security</groupId>
+  <artifactId>spring-security-samples-boot-oauth2login</artifactId>
+  <version>5.0.0.BUILD-SNAPSHOT</version>
+  <name>spring-security-samples-boot-oauth2login</name>
+  <description>spring-security-samples-boot-oauth2login</description>
+  <url>http://spring.io/spring-security</url>
+  <organization>
+    <name>spring.io</name>
+    <url>http://spring.io/</url>
+  </organization>
+  <licenses>
+    <license>
+      <name>The Apache Software License, Version 2.0</name>
+      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+      <distribution>repo</distribution>
+    </license>
+  </licenses>
+  <developers>
+    <developer>
+      <id>rwinch</id>
+      <name>Rob Winch</name>
+      <email>rwinch@pivotal.io</email>
+    </developer>
+    <developer>
+      <id>jgrandja</id>
+      <name>Joe Grandja</name>
+      <email>jgrandja@pivotal.io</email>
+    </developer>
+  </developers>
+  <scm>
+    <connection>scm:git:git://github.com/spring-projects/spring-security</connection>
+    <developerConnection>scm:git:git://github.com/spring-projects/spring-security</developerConnection>
+    <url>https://github.com/spring-projects/spring-security</url>
+  </scm>
+  <dependencyManagement>
+    <dependencies>
+      <dependency>
+        <groupId>org.springframework</groupId>
+        <artifactId>spring-framework-bom</artifactId>
+        <version>4.3.5.RELEASE</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-dependencies</artifactId>
+        <version>1.5.0.BUILD-SNAPSHOT</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
+    </dependencies>
+  </dependencyManagement>
+  <dependencies>
+    <dependency>
+      <groupId>org.springframework.boot</groupId>
+      <artifactId>spring-boot-starter-security</artifactId>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework.boot</groupId>
+      <artifactId>spring-boot-starter-thymeleaf</artifactId>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework.boot</groupId>
+      <artifactId>spring-boot-starter-web</artifactId>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework.security</groupId>
+      <artifactId>spring-security-config</artifactId>
+      <version>5.0.0.BUILD-SNAPSHOT</version>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework.security</groupId>
+      <artifactId>spring-security-oauth2-client</artifactId>
+      <version>5.0.0.BUILD-SNAPSHOT</version>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework.security</groupId>
+      <artifactId>spring-security-web</artifactId>
+      <version>5.0.0.BUILD-SNAPSHOT</version>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.thymeleaf.extras</groupId>
+      <artifactId>thymeleaf-extras-springsecurity4</artifactId>
+      <version>2.1.3.RELEASE</version>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>commons-logging</groupId>
+      <artifactId>commons-logging</artifactId>
+      <version>1.2</version>
+      <scope>compile</scope>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>ch.qos.logback</groupId>
+      <artifactId>logback-classic</artifactId>
+      <version>1.1.2</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.12</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>net.sourceforge.htmlunit</groupId>
+      <artifactId>htmlunit</artifactId>
+      <version>2.24</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.assertj</groupId>
+      <artifactId>assertj-core</artifactId>
+      <version>3.6.2</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <version>1.10.19</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>jcl-over-slf4j</artifactId>
+      <version>1.7.7</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework.boot</groupId>
+      <artifactId>spring-boot-starter-test</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework.security</groupId>
+      <artifactId>spring-security-test</artifactId>
+      <version>5.0.0.BUILD-SNAPSHOT</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework</groupId>
+      <artifactId>spring-test</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+  <repositories>
+    <repository>
+      <id>spring-snapshot</id>
+      <url>https://repo.spring.io/snapshot</url>
+    </repository>
+  </repositories>
+  <build>
+    <plugins>
+      <plugin>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <configuration>
+          <source>1.8</source>
+          <target>1.8</target>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+</project>

+ 15 - 0
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'
+}

+ 405 - 0
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<String, String> 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<String, String> 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<String, String> 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<HtmlAnchor> 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<expectedClients; i++) {
+			assertThat(clientAnchorElements.get(i).getAttribute("href")).isIn(
+				googleClientAuthorizeUri, githubClientAuthorizeUri,
+				facebookClientAuthorizeUri, oktaClientAuthorizeUri);
+			assertThat(clientAnchorElements.get(i).asText()).isIn(
+				this.googleClientRegistration.getClientName(),
+				this.githubClientRegistration.getClientName(),
+				this.facebookClientRegistration.getClientName(),
+				this.oktaClientRegistration.getClientName());
+		}
+	}
+
+	private void assertUserInfoPage(HtmlPage page) throws Exception {
+		assertThat(page.getTitleText()).isEqualTo("Spring Security - OAuth2 User Info");
+
+		DomNodeList<HtmlElement> 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<HtmlAnchor> 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<AuthorizationCodeAuthenticationToken> 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<String, Object> 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 {
+	}
+}

+ 129 - 0
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<ClientRegistration> clientRegistrations = new ArrayList<>();
+
+			Set<String> 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<String> resolveClientPropertyKeys(Environment environment) {
+		Set<String> 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<String> 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)"));
+		}
+	}
+}

+ 108 - 0
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<HttpSecurity> oauth2LoginConfigurer) throws Exception {
+			Set<String> 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<? extends Converter> userInfoConverterType = ClassUtils.resolveClassName(
+						userInfoConverterTypeValue, this.getClass().getClassLoader()).asSubclass(Converter.class);
+					Converter<ClientHttpResponse, ? extends OAuth2User> userInfoConverter = null;
+					if (AbstractOAuth2UserConverter.class.isAssignableFrom(userInfoConverterType)) {
+						Constructor<? extends Converter> oauth2UserConverterConstructor = ClassUtils.getConstructorIfAvailable(userInfoConverterType, String.class);
+						if (oauth2UserConverterConstructor != null) {
+							String userInfoNameAttributeKey = this.environment.getProperty(fullClientPropertyKey + USER_INFO_NAME_ATTR_KEY_PROPERTY);
+							userInfoConverter = (Converter<ClientHttpResponse, ? extends OAuth2User>)oauth2UserConverterConstructor.newInstance(userInfoNameAttributeKey);
+						}
+					}
+					if (userInfoConverter == null) {
+						userInfoConverter = (Converter<ClientHttpResponse, ? extends OAuth2User>)userInfoConverterType.newInstance();
+					}
+					oauth2LoginConfigurer.userInfoEndpoint().userInfoTypeConverter(userInfoConverter, new URI(userInfoUriValue));
+				}
+			}
+		}
+	}
+}

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

+ 85 - 0
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<? extends GrantedAuthority> getAuthorities() {
+		return Collections.emptyList();
+	}
+
+	@Override
+	public Map<String, Object> getAttributes() {
+		Map<String, Object> 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;
+	}
+}

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

+ 44 - 0
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

+ 4 - 0
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

+ 34 - 0
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

+ 33 - 0
samples/boot/oauth2login/src/main/resources/templates/index.html

@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
+<head>
+	<title>Spring Security - OAuth2 User Info</title>
+	<meta charset="utf-8" />
+</head>
+<body>
+<div style="float: right" th:fragment="logout" sec:authorize="isAuthenticated()">
+	<div style="float:left">
+		<span style="font-weight:bold">User: </span><span sec:authentication="name"></span>
+	</div>
+	<div style="float:none">&nbsp;</div>
+	<div style="float:right">
+		<form action="#" th:action="@{/logout}" method="post">
+			<input type="submit" value="Logout" />
+		</form>
+	</div>
+</div>
+<h1>OAuth2 User Info</h1>
+<div>
+	<span style="font-weight:bold">Name: </span><span th:text="${userName}"></span>
+</div>
+<div>&nbsp;</div>
+<div>
+	<span style="font-weight:bold">Attributes:</span>
+	<ul>
+		<li th:each="userAttribute : ${userAttributes}">
+			<span style="font-weight:bold" th:text="${userAttribute.key}"></span>: <span th:text="${userAttribute.value}"></span>
+		</li>
+	</ul>
+</div>
+</body>
+</html>

+ 37 - 13
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<String, String> 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<String, String> 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("<p><font color='red'>Your login attempt was not successful, try again.<br/><br/>Reason: ");
+			sb.append("<p style='color:red;'>Your login attempt was not successful, try again.<br/><br/>Reason: ");
 			sb.append(errorMsg);
-			sb.append("</font></p>");
+			sb.append("</p>");
 		}
 
 		if (logoutSuccess) {
-			sb.append("<p><font color='green'>You have been logged out</font></p>");
+			sb.append("<p style='color:green;'>You have been logged out</p>");
 		}
 
 		if (formLoginEnabled) {
@@ -252,6 +263,19 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
 			sb.append("</form>");
 		}
 
+		if (oauth2LoginEnabled) {
+			sb.append("<h3>Login with OAuth 2.0</h3>");
+			sb.append("<table>\n");
+			for (Map.Entry<String, String> clientAuthenticationUrlToClientName : oauth2AuthenticationUrlToClientName.entrySet()) {
+				sb.append(" <tr><td>");
+				sb.append("<a href=\"").append(clientAuthenticationUrlToClientName.getKey()).append("\">");
+				sb.append(clientAuthenticationUrlToClientName.getValue());
+				sb.append("</a>");
+				sb.append("</td></tr>\n");
+			}
+			sb.append("</table>\n");
+		}
+
 		sb.append("</body></html>");
 
 		return sb.toString();