> adds more powerful request matching support to the XML namespace.
-* https://github.com/spring-projects/spring-security/issues/3990[#3990] - Support for constructing `RoleHierarchy` from `Map` (i.e. `yml`)
-* https://github.com/spring-projects/spring-security/pull/4062[#4062] - Custom cookiePath to `CookieCsrfTokenRepository`
-* https://github.com/spring-projects/spring-security/issues/3794[#3794] - Allow configuration of `InvalidSessionStrategy` on `SessionManagementConfigurer`
-* https://github.com/spring-projects/spring-security/issues/4020[#4020] - Fix Exposing Beans for defaultMethodExpressionHandler can prevent Method Security
-
-=== Miscellaneous
-
-* https://github.com/spring-projects/spring-security/issues/4080[#4080] - Spring 5 support
-* https://github.com/spring-projects/spring-security/issues/4095[#4095] - `Add UserBuilder`
-* https://github.com/spring-projects/spring-security/issues/4018[#4018] - Fix after `csrf()` is invoked, future `MockMvc` invocations use original `CsrfTokenRepository`
-* Version Updates
+== What's New in Spring Security 5.0
+Spring Security 5.0 provides a number of new features as well as support for Spring Framework 5.
+You can find the change log at https://github.com/spring-projects/spring-security/milestone/90?closed=1[5.0.0.M1].
+Below are the highlights of this milestone release.
+
+=== New Features
+
+* https://github.com/spring-projects/spring-security/issues/3907[#3907] - Support added for OAuth 2.0 Login (start with {gh-samples-url}/boot/oauth2login/README.adoc[Sample README])
[[samples]]
== Samples and Guides (Start Here)
diff --git a/oauth2/oauth2-client/pom.xml b/oauth2/oauth2-client/pom.xml
new file mode 100644
index 0000000000..63a98052b3
--- /dev/null
+++ b/oauth2/oauth2-client/pom.xml
@@ -0,0 +1,156 @@
+
+
+ 4.0.0
+ org.springframework.security
+ spring-security-oauth2-client
+ 5.0.0.BUILD-SNAPSHOT
+ spring-security-oauth2-client
+ spring-security-oauth2-client
+ http://spring.io/spring-security
+
+ spring.io
+ http://spring.io/
+
+
+
+ The Apache Software License, Version 2.0
+ http://www.apache.org/licenses/LICENSE-2.0.txt
+ repo
+
+
+
+
+ rwinch
+ Rob Winch
+ rwinch@pivotal.io
+
+
+ jgrandja
+ Joe Grandja
+ jgrandja@pivotal.io
+
+
+
+ scm:git:git://github.com/spring-projects/spring-security
+ scm:git:git://github.com/spring-projects/spring-security
+ https://github.com/spring-projects/spring-security
+
+
+
+
+ org.springframework
+ spring-framework-bom
+ 4.3.5.RELEASE
+ pom
+ import
+
+
+
+
+
+ com.nimbusds
+ oauth2-oidc-sdk
+ 5.21
+ compile
+
+
+ org.springframework.security
+ spring-security-core
+ 5.0.0.BUILD-SNAPSHOT
+ compile
+
+
+ org.springframework.security
+ spring-security-oauth2-core
+ 5.0.0.BUILD-SNAPSHOT
+ compile
+
+
+ org.springframework.security
+ spring-security-web
+ 5.0.0.BUILD-SNAPSHOT
+ compile
+
+
+ org.springframework
+ spring-core
+ compile
+
+
+ commons-logging
+ commons-logging
+
+
+
+
+ org.springframework
+ spring-web
+ compile
+
+
+ commons-logging
+ commons-logging
+ 1.2
+ compile
+ true
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+ ch.qos.logback
+ logback-classic
+ 1.1.2
+ test
+
+
+ junit
+ junit
+ 4.12
+ test
+
+
+ org.assertj
+ assertj-core
+ 3.6.2
+ test
+
+
+ org.mockito
+ mockito-core
+ 1.10.19
+ test
+
+
+ org.slf4j
+ jcl-over-slf4j
+ 1.7.7
+ test
+
+
+ org.springframework
+ spring-test
+ test
+
+
+
+
+ spring-snapshot
+ https://repo.spring.io/snapshot
+
+
+
+
+
+ maven-compiler-plugin
+
+ 1.8
+ 1.8
+
+
+
+
+
diff --git a/oauth2/oauth2-client/spring-security-oauth2-client.gradle b/oauth2/oauth2-client/spring-security-oauth2-client.gradle
new file mode 100644
index 0000000000..b0834f886e
--- /dev/null
+++ b/oauth2/oauth2-client/spring-security-oauth2-client.gradle
@@ -0,0 +1,12 @@
+apply plugin: 'io.spring.convention.spring-module'
+
+dependencies {
+ compile project(':spring-security-core')
+ compile project(':spring-security-oauth2-core')
+ compile project(':spring-security-web')
+ compile springCoreDependency
+ compile 'com.nimbusds:oauth2-oidc-sdk'
+ compile 'org.springframework:spring-web'
+
+ provided 'javax.servlet:javax.servlet-api'
+}
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProcessingFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProcessingFilter.java
new file mode 100644
index 0000000000..bc8c144123
--- /dev/null
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProcessingFilter.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.authentication;
+
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import org.springframework.security.oauth2.client.user.OAuth2UserService;
+import org.springframework.security.oauth2.client.web.converter.AuthorizationCodeAuthorizationResponseAttributesConverter;
+import org.springframework.security.oauth2.client.web.converter.ErrorResponseAttributesConverter;
+import org.springframework.security.oauth2.core.AccessToken;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.endpoint.*;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+import static org.springframework.security.oauth2.client.authentication.AuthorizationCodeRequestRedirectFilter.isDefaultRedirectUri;
+
+/**
+ * An implementation of an {@link AbstractAuthenticationProcessingFilter} that handles
+ * the processing of an OAuth 2.0 Authorization Response for the authorization code grant flow.
+ *
+ *
+ * This Filter processes the Authorization Response in the following step sequence:
+ *
+ *
+ *
+ * Assuming the resource owner (end-user) has granted access to the client, the authorization server will append the
+ * {@link OAuth2Parameter#CODE} and {@link OAuth2Parameter#STATE} (if provided in the Authorization Request ) parameters
+ * to the {@link OAuth2Parameter#REDIRECT_URI} (provided in the Authorization Request )
+ * and redirect the end-user's user-agent back to this Filter (the client).
+ *
+ *
+ * This Filter will then create an {@link AuthorizationCodeAuthenticationToken} with
+ * the {@link OAuth2Parameter#CODE} received in the previous step and pass it to
+ * {@link AuthorizationCodeAuthenticationProvider#authenticate(Authentication)} (indirectly via {@link AuthenticationManager}).
+ * The {@link AuthorizationCodeAuthenticationProvider} will use an {@link AuthorizationGrantTokenExchanger} to make a request
+ * to the authorization server's Token Endpoint for exchanging the {@link OAuth2Parameter#CODE} for an {@link AccessToken}.
+ *
+ *
+ * Upon receiving the Access Token Request , the authorization server will authenticate the client,
+ * verify the {@link OAuth2Parameter#CODE}, and ensure that the {@link OAuth2Parameter#REDIRECT_URI}
+ * received matches the URI originally provided in the Authorization Request .
+ * If the request is valid, the authorization server will respond back with a {@link TokenResponseAttributes}.
+ *
+ *
+ * The {@link AuthorizationCodeAuthenticationProvider} will then create a new {@link OAuth2AuthenticationToken}
+ * associating the {@link AccessToken} from the {@link TokenResponseAttributes} and pass it to
+ * {@link OAuth2UserService#loadUser(OAuth2AuthenticationToken)}. The {@link OAuth2UserService} will make a request
+ * to the authorization server's UserInfo Endpoint (using the {@link AccessToken})
+ * to obtain the end-user's (resource owner) attributes and return it in the form of an {@link OAuth2User}.
+ *
+ *
+ * The {@link AuthorizationCodeAuthenticationProvider} will create another new {@link OAuth2AuthenticationToken}
+ * but this time associating the {@link AccessToken} and {@link OAuth2User} returned from the {@link OAuth2UserService}.
+ * Finally, the {@link OAuth2AuthenticationToken} is returned to the {@link AuthenticationManager}
+ * and then back to this Filter at which point the session is considered "authenticated" .
+ *
+ *
+ *
+ *
+ * NOTE: Steps 4-5 are not part of the authorization code grant flow and instead are
+ * "authentication flow" steps that are required in order to authenticate the end-user with the system.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see AbstractAuthenticationProcessingFilter
+ * @see AuthorizationCodeAuthenticationToken
+ * @see AuthorizationCodeAuthenticationProvider
+ * @see AuthorizationGrantTokenExchanger
+ * @see AuthorizationCodeAuthorizationResponseAttributes
+ * @see AuthorizationRequestAttributes
+ * @see AuthorizationRequestRepository
+ * @see AuthorizationCodeRequestRedirectFilter
+ * @see ClientRegistration
+ * @see ClientRegistrationRepository
+ * @see Section 4.1 Authorization Code Grant Flow
+ * @see Section 4.1.2 Authorization Response
+ */
+public class AuthorizationCodeAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {
+ public static final String AUTHORIZE_BASE_URI = "/oauth2/authorize/code";
+ private static final String CLIENT_ALIAS_VARIABLE_NAME = "clientAlias";
+ private static final String AUTHORIZE_URI = AUTHORIZE_BASE_URI + "/{" + CLIENT_ALIAS_VARIABLE_NAME + "}";
+ private static final String AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE = "authorization_request_not_found";
+ private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
+ private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_parameter";
+ private final ErrorResponseAttributesConverter errorResponseConverter = new ErrorResponseAttributesConverter();
+ private final AuthorizationCodeAuthorizationResponseAttributesConverter authorizationCodeResponseConverter =
+ new AuthorizationCodeAuthorizationResponseAttributesConverter();
+ private final RequestMatcher authorizeRequestMatcher = new AntPathRequestMatcher(AUTHORIZE_URI);
+ private ClientRegistrationRepository clientRegistrationRepository;
+ private AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionAuthorizationRequestRepository();
+
+ public AuthorizationCodeAuthenticationProcessingFilter() {
+ super(AUTHORIZE_URI);
+ }
+
+ @Override
+ public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
+ throws AuthenticationException, IOException, ServletException {
+
+ ErrorResponseAttributes authorizationError = this.errorResponseConverter.convert(request);
+ if (authorizationError != null) {
+ OAuth2Error oauth2Error = new OAuth2Error(authorizationError.getErrorCode(),
+ authorizationError.getDescription(), authorizationError.getUri());
+ this.getAuthorizationRequestRepository().removeAuthorizationRequest(request);
+ throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
+ }
+
+ AuthorizationRequestAttributes matchingAuthorizationRequest = this.resolveAuthorizationRequest(request);
+
+ ClientRegistration clientRegistration = this.getClientRegistrationRepository().getRegistrationByClientId(
+ matchingAuthorizationRequest.getClientId());
+
+ // If clientRegistration.redirectUri is the default one (with Uri template variables)
+ // then use matchingAuthorizationRequest.redirectUri instead
+ if (isDefaultRedirectUri(clientRegistration)) {
+ clientRegistration = new ClientRegistrationBuilderWithUriOverrides(
+ clientRegistration, matchingAuthorizationRequest.getRedirectUri()).build();
+ }
+
+ AuthorizationCodeAuthorizationResponseAttributes authorizationCodeResponseAttributes =
+ this.authorizationCodeResponseConverter.convert(request);
+
+ AuthorizationCodeAuthenticationToken authRequest = new AuthorizationCodeAuthenticationToken(
+ authorizationCodeResponseAttributes.getCode(), clientRegistration);
+
+ authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
+
+ Authentication authenticated = this.getAuthenticationManager().authenticate(authRequest);
+
+ return authenticated;
+ }
+
+ public RequestMatcher getAuthorizeRequestMatcher() {
+ return this.authorizeRequestMatcher;
+ }
+
+ protected ClientRegistrationRepository getClientRegistrationRepository() {
+ return this.clientRegistrationRepository;
+ }
+
+ public final void setClientRegistrationRepository(ClientRegistrationRepository clientRegistrationRepository) {
+ Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
+ Assert.notEmpty(clientRegistrationRepository.getRegistrations(), "clientRegistrationRepository cannot be empty");
+ this.clientRegistrationRepository = clientRegistrationRepository;
+ }
+
+ protected AuthorizationRequestRepository getAuthorizationRequestRepository() {
+ return this.authorizationRequestRepository;
+ }
+
+ public final void setAuthorizationRequestRepository(AuthorizationRequestRepository authorizationRequestRepository) {
+ Assert.notNull(authorizationRequestRepository, "authorizationRequestRepository cannot be null");
+ this.authorizationRequestRepository = authorizationRequestRepository;
+ }
+
+ private AuthorizationRequestAttributes resolveAuthorizationRequest(HttpServletRequest request) {
+ AuthorizationRequestAttributes authorizationRequest =
+ this.getAuthorizationRequestRepository().loadAuthorizationRequest(request);
+ if (authorizationRequest == null) {
+ OAuth2Error oauth2Error = new OAuth2Error(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE);
+ throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
+ }
+ this.getAuthorizationRequestRepository().removeAuthorizationRequest(request);
+ this.assertMatchingAuthorizationRequest(request, authorizationRequest);
+ return authorizationRequest;
+ }
+
+ private void assertMatchingAuthorizationRequest(HttpServletRequest request, AuthorizationRequestAttributes authorizationRequest) {
+ String state = request.getParameter(OAuth2Parameter.STATE);
+ if (!authorizationRequest.getState().equals(state)) {
+ OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE);
+ throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
+ }
+
+ if (!request.getRequestURL().toString().equals(authorizationRequest.getRedirectUri())) {
+ OAuth2Error oauth2Error = new OAuth2Error(INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE);
+ throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
+ }
+ }
+
+ private static class ClientRegistrationBuilderWithUriOverrides extends ClientRegistration.Builder {
+
+ private ClientRegistrationBuilderWithUriOverrides(ClientRegistration clientRegistration, String redirectUri) {
+ super(clientRegistration.getClientId());
+ this.clientSecret(clientRegistration.getClientSecret());
+ this.clientAuthenticationMethod(clientRegistration.getClientAuthenticationMethod());
+ this.authorizedGrantType(clientRegistration.getAuthorizedGrantType());
+ this.redirectUri(redirectUri);
+ if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) {
+ this.scopes(clientRegistration.getScopes().stream().toArray(String[]::new));
+ }
+ this.authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri());
+ this.tokenUri(clientRegistration.getProviderDetails().getTokenUri());
+ this.userInfoUri(clientRegistration.getProviderDetails().getUserInfoUri());
+ this.clientName(clientRegistration.getClientName());
+ this.clientAlias(clientRegistration.getClientAlias());
+ }
+ }
+}
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProvider.java
new file mode 100644
index 0000000000..4ae4fd1665
--- /dev/null
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProvider.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.authentication;
+
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
+import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
+import org.springframework.security.oauth2.client.user.OAuth2UserService;
+import org.springframework.security.oauth2.core.AccessToken;
+import org.springframework.security.oauth2.core.endpoint.TokenResponseAttributes;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.util.Assert;
+
+import java.util.Collection;
+
+/**
+ * An implementation of an {@link AuthenticationProvider} that is responsible for authenticating
+ * an authorization code credential with the authorization server's Token Endpoint
+ * and if valid, exchanging it for an access token credential.
+ * Additionally, it will also obtain the end-user's (resource owner) attributes from the UserInfo Endpoint
+ * (using the access token ) and create a Principal in the form of an {@link OAuth2User}
+ * associating it with the returned {@link OAuth2AuthenticationToken}.
+ *
+ *
+ * The {@link AuthorizationCodeAuthenticationProvider} uses an {@link AuthorizationGrantTokenExchanger}
+ * to make a request to the authorization server's Token Endpoint
+ * to verify the {@link AuthorizationCodeAuthenticationToken#getAuthorizationCode()}.
+ * If the request is valid, the authorization server will respond back with a {@link TokenResponseAttributes}.
+ *
+ *
+ * It will then create a {@link OAuth2AuthenticationToken} associating the {@link AccessToken}
+ * from the {@link TokenResponseAttributes} and pass it to {@link OAuth2UserService#loadUser(OAuth2AuthenticationToken)}
+ * to obtain the end-user's (resource owner) attributes in the form of an {@link OAuth2User}.
+ *
+ *
+ * Finally, it will create another {@link OAuth2AuthenticationToken}, this time associating
+ * the {@link AccessToken} and {@link OAuth2User} and return it to the {@link AuthenticationManager},
+ * at which point the {@link OAuth2AuthenticationToken} is considered "authenticated" .
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see AuthorizationCodeAuthenticationToken
+ * @see AuthorizationGrantTokenExchanger
+ * @see TokenResponseAttributes
+ * @see AccessToken
+ * @see OAuth2UserService
+ * @see OAuth2User
+ * @see Section 4.1 Authorization Code Grant Flow
+ * @see Section 4.1.3 Access Token Request
+ * @see Section 4.1.4 Access Token Response
+ */
+public class AuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
+ private final AuthorizationGrantTokenExchanger authorizationCodeTokenExchanger;
+ private final OAuth2UserService userInfoService;
+ private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
+
+ public AuthorizationCodeAuthenticationProvider(
+ AuthorizationGrantTokenExchanger authorizationCodeTokenExchanger,
+ OAuth2UserService userInfoService) {
+
+ Assert.notNull(authorizationCodeTokenExchanger, "authorizationCodeTokenExchanger cannot be null");
+ Assert.notNull(userInfoService, "userInfoService cannot be null");
+ this.authorizationCodeTokenExchanger = authorizationCodeTokenExchanger;
+ this.userInfoService = userInfoService;
+ }
+
+ @Override
+ public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+ AuthorizationCodeAuthenticationToken authorizationCodeAuthentication =
+ (AuthorizationCodeAuthenticationToken) authentication;
+
+ TokenResponseAttributes tokenResponse =
+ this.authorizationCodeTokenExchanger.exchange(authorizationCodeAuthentication);
+
+ AccessToken accessToken = new AccessToken(tokenResponse.getTokenType(),
+ tokenResponse.getTokenValue(), tokenResponse.getIssuedAt(),
+ tokenResponse.getExpiresAt(), tokenResponse.getScopes());
+ OAuth2AuthenticationToken accessTokenAuthentication = new OAuth2AuthenticationToken(
+ authorizationCodeAuthentication.getClientRegistration(), accessToken);
+ accessTokenAuthentication.setDetails(authorizationCodeAuthentication.getDetails());
+
+ OAuth2User user = this.userInfoService.loadUser(accessTokenAuthentication);
+
+ Collection 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;
+ }
+}
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationToken.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationToken.java
new file mode 100644
index 0000000000..6a510d5ff1
--- /dev/null
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationToken.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.authentication;
+
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.util.Assert;
+
+/**
+ * An implementation of an {@link AuthorizationGrantAuthenticationToken} that holds
+ * an authorization code grant credential for a specific client identified in {@link #getClientRegistration()}.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see AuthorizationGrantAuthenticationToken
+ * @see ClientRegistration
+ * @see Section 1.3.1 Authorization Code Grant
+ */
+public class AuthorizationCodeAuthenticationToken extends AuthorizationGrantAuthenticationToken {
+ private final String authorizationCode;
+ private final ClientRegistration clientRegistration;
+
+ public AuthorizationCodeAuthenticationToken(String authorizationCode, ClientRegistration clientRegistration) {
+ super(AuthorizationGrantType.AUTHORIZATION_CODE, AuthorityUtils.NO_AUTHORITIES);
+ Assert.hasText(authorizationCode, "authorizationCode cannot be empty");
+ Assert.notNull(clientRegistration, "clientRegistration cannot be null");
+ this.authorizationCode = authorizationCode;
+ this.clientRegistration = clientRegistration;
+ this.setAuthenticated(false);
+ }
+
+ @Override
+ public Object getPrincipal() {
+ return null;
+ }
+
+ @Override
+ public Object getCredentials() {
+ return this.getAuthorizationCode();
+ }
+
+ public String getAuthorizationCode() {
+ return this.authorizationCode;
+ }
+
+ public ClientRegistration getClientRegistration() {
+ return this.clientRegistration;
+ }
+}
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeRequestRedirectFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeRequestRedirectFilter.java
new file mode 100644
index 0000000000..48226963c8
--- /dev/null
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeRequestRedirectFilter.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.authentication;
+
+import org.springframework.security.crypto.keygen.StringKeyGenerator;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import org.springframework.security.oauth2.core.endpoint.AuthorizationRequestAttributes;
+import org.springframework.security.web.DefaultRedirectStrategy;
+import org.springframework.security.web.RedirectStrategy;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.util.Assert;
+import org.springframework.web.filter.OncePerRequestFilter;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URI;
+
+import static org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationProcessingFilter.AUTHORIZE_BASE_URI;
+
+/**
+ * This Filter initiates the authorization code grant flow by redirecting
+ * the end-user's user-agent to the authorization server's Authorization Endpoint .
+ *
+ *
+ * It uses an {@link AuthorizationRequestUriBuilder} to build the OAuth 2.0 Authorization Request ,
+ * which is used as the redirect URI to the Authorization Endpoint .
+ * The redirect URI will include the client identifier, requested scope(s), state, response type, and a redirection URI
+ * which the authorization server will send the user-agent back to (handled by {@link AuthorizationCodeAuthenticationProcessingFilter})
+ * once access is granted (or denied) by the end-user (resource owner).
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see AuthorizationRequestAttributes
+ * @see AuthorizationRequestRepository
+ * @see AuthorizationRequestUriBuilder
+ * @see ClientRegistration
+ * @see ClientRegistrationRepository
+ * @see AuthorizationCodeAuthenticationProcessingFilter
+ * @see Section 4.1 Authorization Code Grant Flow
+ * @see Section 4.1.1 Authorization Request
+ */
+public class AuthorizationCodeRequestRedirectFilter extends OncePerRequestFilter {
+ public static final String AUTHORIZATION_BASE_URI = "/oauth2/authorization/code";
+ private static final String CLIENT_ALIAS_VARIABLE_NAME = "clientAlias";
+ private static final String AUTHORIZATION_URI = AUTHORIZATION_BASE_URI + "/{" + CLIENT_ALIAS_VARIABLE_NAME + "}";
+ private static final String DEFAULT_REDIRECT_URI_TEMPLATE = "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}";
+ private final AntPathRequestMatcher authorizationRequestMatcher;
+ private final ClientRegistrationRepository clientRegistrationRepository;
+ private final AuthorizationRequestUriBuilder authorizationUriBuilder;
+ private final RedirectStrategy authorizationRedirectStrategy = new DefaultRedirectStrategy();
+ private final StringKeyGenerator stateGenerator = new DefaultStateGenerator();
+ private AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionAuthorizationRequestRepository();
+
+ public AuthorizationCodeRequestRedirectFilter(ClientRegistrationRepository clientRegistrationRepository,
+ AuthorizationRequestUriBuilder authorizationUriBuilder) {
+
+ Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
+ Assert.notNull(authorizationUriBuilder, "authorizationUriBuilder cannot be null");
+ this.authorizationRequestMatcher = new AntPathRequestMatcher(AUTHORIZATION_URI);
+ this.clientRegistrationRepository = clientRegistrationRepository;
+ this.authorizationUriBuilder = authorizationUriBuilder;
+ }
+
+ public final void setAuthorizationRequestRepository(AuthorizationRequestRepository authorizationRequestRepository) {
+ Assert.notNull(authorizationRequestRepository, "authorizationRequestRepository cannot be null");
+ this.authorizationRequestRepository = authorizationRequestRepository;
+ }
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+ throws ServletException, IOException {
+
+ if (this.requiresAuthorization(request, response)) {
+ try {
+ this.sendRedirectForAuthorization(request, response);
+ } catch (Exception failed) {
+ this.unsuccessfulAuthorization(request, response, failed);
+ }
+ return;
+ }
+
+ filterChain.doFilter(request, response);
+ }
+
+ protected boolean requiresAuthorization(HttpServletRequest request, HttpServletResponse response) {
+ return this.authorizationRequestMatcher.matches(request);
+ }
+
+ protected void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException {
+
+ String clientAlias = this.authorizationRequestMatcher
+ .extractUriTemplateVariables(request).get(CLIENT_ALIAS_VARIABLE_NAME);
+ ClientRegistration clientRegistration = this.clientRegistrationRepository.getRegistrationByClientAlias(clientAlias);
+ if (clientRegistration == null) {
+ throw new IllegalArgumentException("Invalid Client Identifier (Alias): " + clientAlias);
+ }
+
+ String redirectUriStr;
+ if (isDefaultRedirectUri(clientRegistration)) {
+ redirectUriStr = this.expandDefaultRedirectUri(request, clientRegistration);
+ } else {
+ redirectUriStr = clientRegistration.getRedirectUri();
+ }
+
+ AuthorizationRequestAttributes authorizationRequestAttributes =
+ AuthorizationRequestAttributes.withAuthorizationCode()
+ .clientId(clientRegistration.getClientId())
+ .authorizeUri(clientRegistration.getProviderDetails().getAuthorizationUri())
+ .redirectUri(redirectUriStr)
+ .scopes(clientRegistration.getScopes())
+ .state(this.stateGenerator.generateKey())
+ .build();
+
+ this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequestAttributes, request);
+
+ URI redirectUri = this.authorizationUriBuilder.build(authorizationRequestAttributes);
+ this.authorizationRedirectStrategy.sendRedirect(request, response, redirectUri.toString());
+ }
+
+ protected void unsuccessfulAuthorization(HttpServletRequest request, HttpServletResponse response,
+ Exception failed) throws IOException, ServletException {
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("Authorization Request failed: " + failed.toString(), failed);
+ }
+ response.sendError(HttpServletResponse.SC_BAD_REQUEST, failed.getMessage());
+ }
+
+ static boolean isDefaultRedirectUri(ClientRegistration clientRegistration) {
+ return DEFAULT_REDIRECT_URI_TEMPLATE.equals(clientRegistration.getRedirectUri());
+ }
+
+ private String expandDefaultRedirectUri(HttpServletRequest request, ClientRegistration clientRegistration) {
+ return UriComponentsBuilder.fromUriString(DEFAULT_REDIRECT_URI_TEMPLATE)
+ .buildAndExpand(request.getScheme(), request.getServerName(), request.getServerPort(),
+ AUTHORIZE_BASE_URI, clientRegistration.getClientAlias())
+ .encode()
+ .toUriString();
+ }
+}
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationGrantAuthenticationToken.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationGrantAuthenticationToken.java
new file mode 100644
index 0000000000..2e522c936e
--- /dev/null
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationGrantAuthenticationToken.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.authentication;
+
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.SpringSecurityCoreVersion;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.util.Assert;
+
+import java.util.Collection;
+
+/**
+ * Base implementation of an {@link AbstractAuthenticationToken} that holds
+ * an authorization grant credential for a specific {@link AuthorizationGrantType}.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see AuthorizationGrantType
+ * @see Section 1.3 Authorization Grant
+ */
+public abstract class AuthorizationGrantAuthenticationToken extends AbstractAuthenticationToken {
+ private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
+ private final AuthorizationGrantType authorizationGrantType;
+
+ protected AuthorizationGrantAuthenticationToken(AuthorizationGrantType authorizationGrantType,
+ Collection extends GrantedAuthority> authorities) {
+
+ super(authorities);
+ Assert.notNull(authorizationGrantType, "authorizationGrantType cannot be null");
+ this.authorizationGrantType = authorizationGrantType;
+ }
+
+ public AuthorizationGrantType getGrantType() {
+ return this.authorizationGrantType;
+ }
+}
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationGrantTokenExchanger.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationGrantTokenExchanger.java
new file mode 100644
index 0000000000..b5980bde6b
--- /dev/null
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationGrantTokenExchanger.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.authentication;
+
+
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.endpoint.TokenResponseAttributes;
+
+/**
+ * Implementations of this interface are responsible for "exchanging"
+ * an authorization grant credential (for example, an authorization code) for an
+ * access token credential at the authorization server's Token Endpoint .
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see AuthorizationGrantType
+ * @see AuthorizationGrantAuthenticationToken
+ * @see TokenResponseAttributes
+ * @see Section 1.3 Authorization Grant
+ * @see Section 4.1.3 Access Token Request (Authorization Code Grant)
+ * @see Section 4.1.4 Access Token Response (Authorization Code Grant)
+ */
+public interface AuthorizationGrantTokenExchanger {
+
+ TokenResponseAttributes exchange(T authorizationGrantAuthentication) throws OAuth2AuthenticationException;
+
+}
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationRequestRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationRequestRepository.java
new file mode 100644
index 0000000000..3954a03bb5
--- /dev/null
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationRequestRepository.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.authentication;
+
+import org.springframework.security.oauth2.core.endpoint.AuthorizationRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Implementations of this interface are responsible for the persistence
+ * of {@link AuthorizationRequestAttributes} between requests.
+ *
+ *
+ * Used by the {@link AuthorizationCodeRequestRedirectFilter} for persisting the Authorization Request
+ * before it initiates the authorization code grant flow.
+ * As well, used by the {@link AuthorizationCodeAuthenticationProcessingFilter} when resolving
+ * the associated Authorization Request during the handling of the Authorization Response .
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see AuthorizationRequestAttributes
+ * @see HttpSessionAuthorizationRequestRepository
+ */
+public interface AuthorizationRequestRepository {
+
+ AuthorizationRequestAttributes loadAuthorizationRequest(HttpServletRequest request);
+
+ void saveAuthorizationRequest(AuthorizationRequestAttributes authorizationRequest, HttpServletRequest request);
+
+ AuthorizationRequestAttributes removeAuthorizationRequest(HttpServletRequest request);
+
+}
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationRequestUriBuilder.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationRequestUriBuilder.java
new file mode 100644
index 0000000000..2eba15386c
--- /dev/null
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationRequestUriBuilder.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.authentication;
+
+
+import org.springframework.security.oauth2.core.endpoint.AuthorizationRequestAttributes;
+
+import java.net.URI;
+
+/**
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ */
+public interface AuthorizationRequestUriBuilder {
+
+ URI build(AuthorizationRequestAttributes authorizationRequestAttributes);
+}
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/DefaultAuthorizationRequestUriBuilder.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/DefaultAuthorizationRequestUriBuilder.java
new file mode 100644
index 0000000000..e2ae810873
--- /dev/null
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/DefaultAuthorizationRequestUriBuilder.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.authentication;
+
+import org.springframework.security.oauth2.core.endpoint.OAuth2Parameter;
+import org.springframework.security.oauth2.core.endpoint.ResponseType;
+import org.springframework.security.oauth2.core.endpoint.AuthorizationRequestAttributes;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.net.URI;
+import java.util.stream.Collectors;
+
+/**
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ */
+public class DefaultAuthorizationRequestUriBuilder implements AuthorizationRequestUriBuilder {
+
+ @Override
+ public URI build(AuthorizationRequestAttributes authorizationRequestAttributes) {
+ UriComponentsBuilder uriBuilder = UriComponentsBuilder
+ .fromUriString(authorizationRequestAttributes.getAuthorizeUri())
+ .queryParam(OAuth2Parameter.RESPONSE_TYPE, ResponseType.CODE.value());
+ if (authorizationRequestAttributes.getRedirectUri() != null) {
+ uriBuilder.queryParam(OAuth2Parameter.REDIRECT_URI, authorizationRequestAttributes.getRedirectUri());
+ }
+ uriBuilder
+ .queryParam(OAuth2Parameter.CLIENT_ID, authorizationRequestAttributes.getClientId())
+ .queryParam(OAuth2Parameter.SCOPE,
+ authorizationRequestAttributes.getScopes().stream().collect(Collectors.joining(" ")))
+ .queryParam(OAuth2Parameter.STATE, authorizationRequestAttributes.getState());
+
+ return uriBuilder.build().encode().toUri();
+ }
+}
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/DefaultStateGenerator.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/DefaultStateGenerator.java
new file mode 100644
index 0000000000..095ba53bd7
--- /dev/null
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/DefaultStateGenerator.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.authentication;
+
+import org.springframework.security.crypto.codec.Base64;
+import org.springframework.security.crypto.keygen.BytesKeyGenerator;
+import org.springframework.security.crypto.keygen.KeyGenerators;
+import org.springframework.security.crypto.keygen.StringKeyGenerator;
+import org.springframework.util.Assert;
+
+/**
+ * The default implementation for generating the
+ * {@link org.springframework.security.oauth2.core.endpoint.OAuth2Parameter#STATE} parameter
+ * used in the Authorization Request and correlated in the Authorization Response (or Error Response ).
+ *
+ *
+ * NOTE: The value of the state parameter is an opaque String
+ * used by the client to prevent cross-site request forgery, as described in
+ * Section 10.12 of the specification.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ */
+public class DefaultStateGenerator implements StringKeyGenerator {
+ private static final int DEFAULT_BYTE_LENGTH = 32;
+ private final BytesKeyGenerator keyGenerator;
+
+ public DefaultStateGenerator() {
+ this(DEFAULT_BYTE_LENGTH);
+ }
+
+ public DefaultStateGenerator(int byteLength) {
+ Assert.isTrue(byteLength > 0, "byteLength must be greater than 0");
+ this.keyGenerator = KeyGenerators.secureRandom(byteLength);
+ }
+
+ @Override
+ public String generateKey() {
+ return new String(Base64.encode(keyGenerator.generateKey()));
+ }
+}
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/HttpSessionAuthorizationRequestRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/HttpSessionAuthorizationRequestRepository.java
new file mode 100644
index 0000000000..dfc343d9b0
--- /dev/null
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/HttpSessionAuthorizationRequestRepository.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.authentication;
+
+import org.springframework.security.oauth2.core.endpoint.AuthorizationRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+
+/**
+ * An implementation of an {@link AuthorizationRequestRepository} that stores
+ * {@link AuthorizationRequestAttributes} in the {@link HttpSession}.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see AuthorizationRequestAttributes
+ */
+public final class HttpSessionAuthorizationRequestRepository implements AuthorizationRequestRepository {
+ private static final String DEFAULT_AUTHORIZATION_REQUEST_ATTR_NAME =
+ HttpSessionAuthorizationRequestRepository.class.getName() + ".AUTHORIZATION_REQUEST";
+ private String sessionAttributeName = DEFAULT_AUTHORIZATION_REQUEST_ATTR_NAME;
+
+ @Override
+ public AuthorizationRequestAttributes loadAuthorizationRequest(HttpServletRequest request) {
+ AuthorizationRequestAttributes authorizationRequest = null;
+ HttpSession session = request.getSession(false);
+ if (session != null) {
+ authorizationRequest = (AuthorizationRequestAttributes) session.getAttribute(this.sessionAttributeName);
+ }
+ return authorizationRequest;
+ }
+
+ @Override
+ public void saveAuthorizationRequest(AuthorizationRequestAttributes authorizationRequest, HttpServletRequest request) {
+ if (authorizationRequest == null) {
+ this.removeAuthorizationRequest(request);
+ return;
+ }
+ request.getSession().setAttribute(this.sessionAttributeName, authorizationRequest);
+ }
+
+ @Override
+ public AuthorizationRequestAttributes removeAuthorizationRequest(HttpServletRequest request) {
+ AuthorizationRequestAttributes authorizationRequest = this.loadAuthorizationRequest(request);
+ if (authorizationRequest != null) {
+ request.getSession().removeAttribute(this.sessionAttributeName);
+ }
+ return authorizationRequest;
+ }
+}
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationException.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationException.java
new file mode 100644
index 0000000000..43465923d5
--- /dev/null
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationException.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.authentication;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.util.Assert;
+
+/**
+ * This exception is thrown for all OAuth 2.0 related {@link Authentication} errors.
+ *
+ *
+ * There are a number of scenarios where an error may occur, for example:
+ *
+ * The authorization request or token request is missing a required parameter
+ * Missing or invalid client identifier
+ * Invalid or mismatching redirection URI
+ * The requested scope is invalid, unknown, or malformed
+ * The resource owner or authorization server denied the access request
+ * Client authentication failed
+ * The provided authorization grant (authorization code, resource owner credentials) is invalid, expired, or revoked
+ *
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ */
+public class OAuth2AuthenticationException extends AuthenticationException {
+ private OAuth2Error errorObject;
+
+ public OAuth2AuthenticationException(OAuth2Error errorObject, Throwable cause) {
+ this(errorObject, cause.getMessage(), cause);
+ }
+
+ public OAuth2AuthenticationException(OAuth2Error errorObject, String message) {
+ super(message);
+ this.setErrorObject(errorObject);
+ }
+
+ public OAuth2AuthenticationException(OAuth2Error errorObject, String message, Throwable cause) {
+ super(message, cause);
+ this.setErrorObject(errorObject);
+ }
+
+ public OAuth2Error getErrorObject() {
+ return errorObject;
+ }
+
+ private void setErrorObject(OAuth2Error errorObject) {
+ Assert.notNull(errorObject, "OAuth2 Error object cannot be null");
+ this.errorObject = errorObject;
+ }
+}
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationToken.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationToken.java
new file mode 100644
index 0000000000..d5119e3312
--- /dev/null
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationToken.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.authentication;
+
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.SpringSecurityCoreVersion;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.user.OAuth2UserService;
+import org.springframework.security.oauth2.core.AccessToken;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.util.Assert;
+
+import java.util.Collection;
+
+/**
+ * An implementation of an {@link AbstractAuthenticationToken}
+ * that represents an OAuth 2.0 {@link Authentication}.
+ *
+ *
+ * It associates an {@link OAuth2User}, {@link ClientRegistration} and an {@link AccessToken}.
+ * This Authentication is considered "authenticated" if the {@link OAuth2User}
+ * is provided in the respective constructor. This typically happens after the {@link OAuth2UserService}
+ * retrieves the end-user's (resource owner) attributes from the UserInfo Endpoint .
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see OAuth2User
+ * @see ClientRegistration
+ * @see AccessToken
+ */
+public class OAuth2AuthenticationToken extends AbstractAuthenticationToken {
+ private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
+ private final OAuth2User principal;
+ private final ClientRegistration clientRegistration;
+ private final AccessToken accessToken;
+
+ public OAuth2AuthenticationToken(ClientRegistration clientRegistration, AccessToken accessToken) {
+ this(null, AuthorityUtils.NO_AUTHORITIES, clientRegistration, accessToken);
+ }
+
+ public OAuth2AuthenticationToken(OAuth2User principal, Collection 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;
+ }
+}
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/nimbus/NimbusAuthorizationCodeTokenExchanger.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/nimbus/NimbusAuthorizationCodeTokenExchanger.java
new file mode 100644
index 0000000000..3b73d945b8
--- /dev/null
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/nimbus/NimbusAuthorizationCodeTokenExchanger.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.authentication.nimbus;
+
+
+import com.nimbusds.oauth2.sdk.*;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretPost;
+import com.nimbusds.oauth2.sdk.auth.Secret;
+import com.nimbusds.oauth2.sdk.http.HTTPRequest;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import org.springframework.http.MediaType;
+import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationToken;
+import org.springframework.security.oauth2.client.authentication.AuthorizationGrantTokenExchanger;
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.core.AccessToken;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.endpoint.TokenResponseAttributes;
+import org.springframework.util.CollectionUtils;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * An implementation of an {@link AuthorizationGrantTokenExchanger} that "exchanges"
+ * an authorization code credential for an access token credential
+ * at the authorization server's Token Endpoint .
+ *
+ *
+ * NOTE: This implementation uses the Nimbus OAuth 2.0 SDK internally.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see AuthorizationCodeAuthenticationToken
+ * @see TokenResponseAttributes
+ * @see Nimbus OAuth 2.0 SDK
+ * @see Section 4.1.3 Access Token Request (Authorization Code Grant)
+ * @see Section 4.1.4 Access Token Response (Authorization Code Grant)
+ */
+public class NimbusAuthorizationCodeTokenExchanger implements AuthorizationGrantTokenExchanger {
+ private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response";
+
+ @Override
+ public TokenResponseAttributes exchange(AuthorizationCodeAuthenticationToken authorizationCodeAuthenticationToken)
+ throws OAuth2AuthenticationException {
+
+ ClientRegistration clientRegistration = authorizationCodeAuthenticationToken.getClientRegistration();
+
+ // Build the authorization code grant request for the token endpoint
+ AuthorizationCode authorizationCode = new AuthorizationCode(authorizationCodeAuthenticationToken.getAuthorizationCode());
+ URI redirectUri = this.toURI(clientRegistration.getRedirectUri());
+ AuthorizationGrant authorizationCodeGrant = new AuthorizationCodeGrant(authorizationCode, redirectUri);
+ URI tokenUri = this.toURI(clientRegistration.getProviderDetails().getTokenUri());
+
+ // Set the credentials to authenticate the client at the token endpoint
+ ClientID clientId = new ClientID(clientRegistration.getClientId());
+ Secret clientSecret = new Secret(clientRegistration.getClientSecret());
+ ClientAuthentication clientAuthentication;
+ if (ClientAuthenticationMethod.FORM.equals(clientRegistration.getClientAuthenticationMethod())) {
+ clientAuthentication = new ClientSecretPost(clientId, clientSecret);
+ } else {
+ clientAuthentication = new ClientSecretBasic(clientId, clientSecret);
+ }
+
+ TokenResponse tokenResponse;
+ try {
+ // Send the Access Token request
+ TokenRequest tokenRequest = new TokenRequest(tokenUri, clientAuthentication, authorizationCodeGrant);
+ HTTPRequest httpRequest = tokenRequest.toHTTPRequest();
+ httpRequest.setAccept(MediaType.APPLICATION_JSON_VALUE);
+ tokenResponse = TokenResponse.parse(httpRequest.send());
+ } catch (ParseException pe) {
+ // This error occurs if the Access Token Response is not well-formed,
+ // for example, a required attribute is missing
+ throw new OAuth2AuthenticationException(new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE), pe);
+ } catch (IOException ioe) {
+ // This error occurs when there is a network-related issue
+ throw new AuthenticationServiceException("An error occurred while sending the Access Token Request: " +
+ ioe.getMessage(), ioe);
+ }
+
+ if (!tokenResponse.indicatesSuccess()) {
+ TokenErrorResponse tokenErrorResponse = (TokenErrorResponse) tokenResponse;
+ ErrorObject errorObject = tokenErrorResponse.getErrorObject();
+ OAuth2Error oauth2Error = new OAuth2Error(errorObject.getCode(), errorObject.getDescription(),
+ (errorObject.getURI() != null ? errorObject.getURI().toString() : null));
+ throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
+ }
+
+ AccessTokenResponse accessTokenResponse = (AccessTokenResponse) tokenResponse;
+
+ String accessToken = accessTokenResponse.getTokens().getAccessToken().getValue();
+ AccessToken.TokenType accessTokenType = null;
+ if (AccessToken.TokenType.BEARER.value().equals(accessTokenResponse.getTokens().getAccessToken().getType().getValue())) {
+ accessTokenType = AccessToken.TokenType.BEARER;
+ }
+ long expiresIn = accessTokenResponse.getTokens().getAccessToken().getLifetime();
+ Set scopes = Collections.emptySet();
+ if (!CollectionUtils.isEmpty(accessTokenResponse.getTokens().getAccessToken().getScope())) {
+ scopes = new HashSet<>(accessTokenResponse.getTokens().getAccessToken().getScope().toStringList());
+ }
+ Map additionalParameters = accessTokenResponse.getCustomParameters().entrySet().stream()
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+
+ return TokenResponseAttributes.withToken(accessToken)
+ .tokenType(accessTokenType)
+ .expiresIn(expiresIn)
+ .scopes(scopes)
+ .additionalParameters(additionalParameters)
+ .build();
+ }
+
+ private URI toURI(String uriStr) {
+ try {
+ return new URI(uriStr);
+ } catch (Exception ex) {
+ throw new IllegalArgumentException("An error occurred parsing URI: " + uriStr, ex);
+ }
+ }
+}
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java
new file mode 100644
index 0000000000..da5b3b2ad3
--- /dev/null
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.registration;
+
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+
+/**
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ */
+public class ClientRegistration {
+ private String clientId;
+ private String clientSecret;
+ private ClientAuthenticationMethod clientAuthenticationMethod = ClientAuthenticationMethod.HEADER;
+ private AuthorizationGrantType authorizedGrantType;
+ private String redirectUri;
+ private Set scopes = Collections.emptySet();
+ private ProviderDetails providerDetails = new ProviderDetails();
+ private String clientName;
+ private String clientAlias;
+
+ protected ClientRegistration() {
+ }
+
+ public String getClientId() {
+ return this.clientId;
+ }
+
+ protected void setClientId(String clientId) {
+ this.clientId = clientId;
+ }
+
+ public String getClientSecret() {
+ return this.clientSecret;
+ }
+
+ protected void setClientSecret(String clientSecret) {
+ this.clientSecret = clientSecret;
+ }
+
+ public ClientAuthenticationMethod getClientAuthenticationMethod() {
+ return this.clientAuthenticationMethod;
+ }
+
+ protected void setClientAuthenticationMethod(ClientAuthenticationMethod clientAuthenticationMethod) {
+ this.clientAuthenticationMethod = clientAuthenticationMethod;
+ }
+
+ public AuthorizationGrantType getAuthorizedGrantType() {
+ return this.authorizedGrantType;
+ }
+
+ protected void setAuthorizedGrantType(AuthorizationGrantType authorizedGrantType) {
+ this.authorizedGrantType = authorizedGrantType;
+ }
+
+ public String getRedirectUri() {
+ return this.redirectUri;
+ }
+
+ protected void setRedirectUri(String redirectUri) {
+ this.redirectUri = redirectUri;
+ }
+
+ public Set getScopes() {
+ return this.scopes;
+ }
+
+ protected void setScopes(Set scopes) {
+ this.scopes = scopes;
+ }
+
+ public ProviderDetails getProviderDetails() {
+ return this.providerDetails;
+ }
+
+ protected void setProviderDetails(ProviderDetails providerDetails) {
+ this.providerDetails = providerDetails;
+ }
+
+ public String getClientName() {
+ return this.clientName;
+ }
+
+ protected void setClientName(String clientName) {
+ this.clientName = clientName;
+ }
+
+ public String getClientAlias() {
+ return this.clientAlias;
+ }
+
+ protected void setClientAlias(String clientAlias) {
+ this.clientAlias = clientAlias;
+ }
+
+ public class ProviderDetails {
+ private String authorizationUri;
+ private String tokenUri;
+ private String userInfoUri;
+
+ protected ProviderDetails() {
+ }
+
+ public String getAuthorizationUri() {
+ return this.authorizationUri;
+ }
+
+ protected void setAuthorizationUri(String authorizationUri) {
+ this.authorizationUri = authorizationUri;
+ }
+
+ public String getTokenUri() {
+ return this.tokenUri;
+ }
+
+ protected void setTokenUri(String tokenUri) {
+ this.tokenUri = tokenUri;
+ }
+
+ public String getUserInfoUri() {
+ return this.userInfoUri;
+ }
+
+ protected void setUserInfoUri(String userInfoUri) {
+ this.userInfoUri = userInfoUri;
+ }
+ }
+
+ public static class Builder {
+ protected String clientId;
+ protected String clientSecret;
+ protected ClientAuthenticationMethod clientAuthenticationMethod = ClientAuthenticationMethod.HEADER;
+ protected AuthorizationGrantType authorizedGrantType;
+ protected String redirectUri;
+ protected Set scopes;
+ protected String authorizationUri;
+ protected String tokenUri;
+ protected String userInfoUri;
+ protected String clientName;
+ protected String clientAlias;
+
+ public Builder(String clientId) {
+ this.clientId = clientId;
+ }
+
+ public Builder(ClientRegistrationProperties clientRegistrationProperties) {
+ this(clientRegistrationProperties.getClientId());
+ this.clientSecret(clientRegistrationProperties.getClientSecret());
+ this.clientAuthenticationMethod(clientRegistrationProperties.getClientAuthenticationMethod());
+ this.authorizedGrantType(clientRegistrationProperties.getAuthorizedGrantType());
+ this.redirectUri(clientRegistrationProperties.getRedirectUri());
+ if (!CollectionUtils.isEmpty(clientRegistrationProperties.getScopes())) {
+ this.scopes(clientRegistrationProperties.getScopes().stream().toArray(String[]::new));
+ }
+ this.authorizationUri(clientRegistrationProperties.getAuthorizationUri());
+ this.tokenUri(clientRegistrationProperties.getTokenUri());
+ this.userInfoUri(clientRegistrationProperties.getUserInfoUri());
+ this.clientName(clientRegistrationProperties.getClientName());
+ this.clientAlias(clientRegistrationProperties.getClientAlias());
+ }
+
+ public Builder clientSecret(String clientSecret) {
+ this.clientSecret = clientSecret;
+ return this;
+ }
+
+ public Builder clientAuthenticationMethod(ClientAuthenticationMethod clientAuthenticationMethod) {
+ this.clientAuthenticationMethod = clientAuthenticationMethod;
+ return this;
+ }
+
+ public Builder authorizedGrantType(AuthorizationGrantType authorizedGrantType) {
+ this.authorizedGrantType = authorizedGrantType;
+ return this;
+ }
+
+ public Builder redirectUri(String redirectUri) {
+ this.redirectUri = redirectUri;
+ return this;
+ }
+
+ public Builder scopes(String... scopes) {
+ if (scopes != null && scopes.length > 0) {
+ this.scopes = Collections.unmodifiableSet(
+ new LinkedHashSet<>(Arrays.asList(scopes)));
+ }
+ return this;
+ }
+
+ public Builder authorizationUri(String authorizationUri) {
+ this.authorizationUri = authorizationUri;
+ return this;
+ }
+
+ public Builder tokenUri(String tokenUri) {
+ this.tokenUri = tokenUri;
+ return this;
+ }
+
+ public Builder userInfoUri(String userInfoUri) {
+ this.userInfoUri = userInfoUri;
+ return this;
+ }
+
+ public Builder clientName(String clientName) {
+ this.clientName = clientName;
+ return this;
+ }
+
+ public Builder clientAlias(String clientAlias) {
+ this.clientAlias = clientAlias;
+ return this;
+ }
+
+ public ClientRegistration build() {
+ this.validateClientWithAuthorizationCodeGrantType();
+ ClientRegistration clientRegistration = new ClientRegistration();
+ this.setProperties(clientRegistration);
+ return clientRegistration;
+ }
+
+ protected void setProperties(ClientRegistration clientRegistration) {
+ clientRegistration.setClientId(this.clientId);
+ clientRegistration.setClientSecret(this.clientSecret);
+ clientRegistration.setClientAuthenticationMethod(this.clientAuthenticationMethod);
+ clientRegistration.setAuthorizedGrantType(this.authorizedGrantType);
+ clientRegistration.setRedirectUri(this.redirectUri);
+ clientRegistration.setScopes(this.scopes);
+
+ ProviderDetails providerDetails = clientRegistration.new ProviderDetails();
+ providerDetails.setAuthorizationUri(this.authorizationUri);
+ providerDetails.setTokenUri(this.tokenUri);
+ providerDetails.setUserInfoUri(this.userInfoUri);
+ clientRegistration.setProviderDetails(providerDetails);
+
+ clientRegistration.setClientName(this.clientName);
+ clientRegistration.setClientAlias(this.clientAlias);
+ }
+
+ protected void validateClientWithAuthorizationCodeGrantType() {
+ Assert.isTrue(AuthorizationGrantType.AUTHORIZATION_CODE.equals(this.authorizedGrantType),
+ "authorizedGrantType must be " + AuthorizationGrantType.AUTHORIZATION_CODE.value());
+ Assert.hasText(this.clientId, "clientId cannot be empty");
+ Assert.hasText(this.clientSecret, "clientSecret cannot be empty");
+ Assert.notNull(this.clientAuthenticationMethod, "clientAuthenticationMethod cannot be null");
+ Assert.hasText(this.redirectUri, "redirectUri cannot be empty");
+ Assert.notEmpty(this.scopes, "scopes cannot be empty");
+ Assert.hasText(this.authorizationUri, "authorizationUri cannot be empty");
+ Assert.hasText(this.tokenUri, "tokenUri cannot be empty");
+ Assert.hasText(this.userInfoUri, "userInfoUri cannot be empty");
+ Assert.hasText(this.clientName, "clientName cannot be empty");
+ Assert.hasText(this.clientAlias, "clientAlias cannot be empty");
+ }
+ }
+}
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrationProperties.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrationProperties.java
new file mode 100644
index 0000000000..a4c73fb3b7
--- /dev/null
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrationProperties.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.registration;
+
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+
+import java.util.Set;
+
+/**
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ */
+public class ClientRegistrationProperties {
+ private String clientId;
+ private String clientSecret;
+ private ClientAuthenticationMethod clientAuthenticationMethod = ClientAuthenticationMethod.HEADER;
+ private AuthorizationGrantType authorizedGrantType;
+ private String redirectUri;
+ private Set scopes;
+ private String authorizationUri;
+ private String tokenUri;
+ private String userInfoUri;
+ private String clientName;
+ private String clientAlias;
+
+
+ public String getClientId() {
+ return this.clientId;
+ }
+
+ public void setClientId(String clientId) {
+ this.clientId = clientId;
+ }
+
+ public String getClientSecret() {
+ return this.clientSecret;
+ }
+
+ public void setClientSecret(String clientSecret) {
+ this.clientSecret = clientSecret;
+ }
+
+ public ClientAuthenticationMethod getClientAuthenticationMethod() {
+ return this.clientAuthenticationMethod;
+ }
+
+ public void setClientAuthenticationMethod(ClientAuthenticationMethod clientAuthenticationMethod) {
+ this.clientAuthenticationMethod = clientAuthenticationMethod;
+ }
+
+ public AuthorizationGrantType getAuthorizedGrantType() {
+ return this.authorizedGrantType;
+ }
+
+ public void setAuthorizedGrantType(AuthorizationGrantType authorizedGrantType) {
+ this.authorizedGrantType = authorizedGrantType;
+ }
+
+ public String getRedirectUri() {
+ return this.redirectUri;
+ }
+
+ public void setRedirectUri(String redirectUri) {
+ this.redirectUri = redirectUri;
+ }
+
+ public Set getScopes() {
+ return this.scopes;
+ }
+
+ public void setScopes(Set scopes) {
+ this.scopes = scopes;
+ }
+
+ public String getAuthorizationUri() {
+ return this.authorizationUri;
+ }
+
+ public void setAuthorizationUri(String authorizationUri) {
+ this.authorizationUri = authorizationUri;
+ }
+
+ public String getTokenUri() {
+ return this.tokenUri;
+ }
+
+ public void setTokenUri(String tokenUri) {
+ this.tokenUri = tokenUri;
+ }
+
+ public String getUserInfoUri() {
+ return this.userInfoUri;
+ }
+
+ public void setUserInfoUri(String userInfoUri) {
+ this.userInfoUri = userInfoUri;
+ }
+
+ public String getClientName() {
+ return this.clientName;
+ }
+
+ public void setClientName(String clientName) {
+ this.clientName = clientName;
+ }
+
+ public String getClientAlias() {
+ return this.clientAlias;
+ }
+
+ public void setClientAlias(String clientAlias) {
+ this.clientAlias = clientAlias;
+ }
+}
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrationRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrationRepository.java
new file mode 100644
index 0000000000..acfab16adf
--- /dev/null
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrationRepository.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.registration;
+
+import java.util.List;
+
+/**
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ */
+public interface ClientRegistrationRepository {
+
+ ClientRegistration getRegistrationByClientId(String clientId);
+
+ ClientRegistration getRegistrationByClientAlias(String clientAlias);
+
+ List getRegistrations();
+
+}
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/InMemoryClientRegistrationRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/InMemoryClientRegistrationRepository.java
new file mode 100644
index 0000000000..3cd21fa51a
--- /dev/null
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/InMemoryClientRegistrationRepository.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.registration;
+
+import org.springframework.util.Assert;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ */
+public final class InMemoryClientRegistrationRepository implements ClientRegistrationRepository {
+ private final List clientRegistrations;
+
+ public InMemoryClientRegistrationRepository(List clientRegistrations) {
+ Assert.notEmpty(clientRegistrations, "clientRegistrations cannot be empty");
+ this.clientRegistrations = Collections.unmodifiableList(clientRegistrations);
+ }
+
+ @Override
+ public ClientRegistration getRegistrationByClientId(String clientId) {
+ Optional clientRegistration =
+ this.clientRegistrations.stream()
+ .filter(c -> c.getClientId().equals(clientId))
+ .findFirst();
+ return clientRegistration.isPresent() ? clientRegistration.get() : null;
+ }
+
+ @Override
+ public ClientRegistration getRegistrationByClientAlias(String clientAlias) {
+ Optional clientRegistration =
+ this.clientRegistrations.stream()
+ .filter(c -> c.getClientAlias().equals(clientAlias))
+ .findFirst();
+ return clientRegistration.isPresent() ? clientRegistration.get() : null;
+ }
+
+ @Override
+ public List getRegistrations() {
+ return this.clientRegistrations;
+ }
+}
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/OAuth2UserService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/OAuth2UserService.java
new file mode 100644
index 0000000000..d4b9d8a964
--- /dev/null
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/OAuth2UserService.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.user;
+
+import org.springframework.security.core.AuthenticatedPrincipal;
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.security.oauth2.oidc.user.UserInfo;
+
+/**
+ * Implementations of this interface are responsible for obtaining
+ * the end-user's (resource owner) attributes from the UserInfo Endpoint
+ * using the provided {@link OAuth2AuthenticationToken#getAccessToken()}
+ * and returning an {@link AuthenticatedPrincipal} in the form of an {@link OAuth2User}
+ * (for a standard OAuth 2.0 Provider ) or {@link UserInfo} (for an OpenID Connect 1.0 Provider ).
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see OAuth2AuthenticationToken
+ * @see AuthenticatedPrincipal
+ * @see OAuth2User
+ * @see UserInfo
+ */
+public interface OAuth2UserService {
+
+ OAuth2User loadUser(OAuth2AuthenticationToken token) throws OAuth2AuthenticationException;
+
+}
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/AbstractOAuth2UserConverter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/AbstractOAuth2UserConverter.java
new file mode 100644
index 0000000000..1c94df7299
--- /dev/null
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/AbstractOAuth2UserConverter.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.user.converter;
+
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ */
+public abstract class AbstractOAuth2UserConverter implements Converter {
+ private final HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
+
+ protected AbstractOAuth2UserConverter() {
+ }
+
+ @Override
+ public final T convert(ClientHttpResponse clientHttpResponse) {
+ Map userAttributes;
+
+ try {
+ userAttributes = (Map) this.jackson2HttpMessageConverter.read(Map.class, clientHttpResponse);
+ } catch (IOException ex) {
+ throw new IllegalArgumentException("An error occurred reading the UserInfo response: " + ex.getMessage(), ex);
+ }
+
+ return this.convert(userAttributes);
+ }
+
+ protected abstract T convert(Map userAttributes);
+
+}
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/CustomOAuth2UserConverter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/CustomOAuth2UserConverter.java
new file mode 100644
index 0000000000..48f4d75ca7
--- /dev/null
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/CustomOAuth2UserConverter.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.user.converter;
+
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+
+import java.io.IOException;
+
+/**
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ */
+public final class CustomOAuth2UserConverter implements Converter {
+ private final HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
+ private final Class customType;
+
+ public CustomOAuth2UserConverter(Class customType) {
+ this.customType = customType;
+ }
+
+ @Override
+ public T convert(ClientHttpResponse clientHttpResponse) {
+ T user;
+
+ try {
+ user = (T) this.jackson2HttpMessageConverter.read(this.customType, clientHttpResponse);
+ } catch (IOException ex) {
+ throw new IllegalArgumentException("An error occurred reading the UserInfo response: " + ex.getMessage(), ex);
+ }
+
+ return user;
+ }
+}
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/OAuth2UserConverter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/OAuth2UserConverter.java
new file mode 100644
index 0000000000..6db9b2bded
--- /dev/null
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/OAuth2UserConverter.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.user.converter;
+
+import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.util.Assert;
+
+import java.util.Map;
+
+/**
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ */
+public final class OAuth2UserConverter extends AbstractOAuth2UserConverter {
+ private final String nameAttributeKey;
+
+ public OAuth2UserConverter(String nameAttributeKey) {
+ Assert.hasText(nameAttributeKey, "nameAttributeKey cannot be empty");
+ this.nameAttributeKey = nameAttributeKey;
+ }
+
+ @Override
+ protected OAuth2User convert(Map userAttributes) {
+ return new DefaultOAuth2User(userAttributes, this.nameAttributeKey);
+ }
+}
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/UserInfoConverter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/UserInfoConverter.java
new file mode 100644
index 0000000000..481ac8c4d9
--- /dev/null
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/UserInfoConverter.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.user.converter;
+
+import org.springframework.security.oauth2.oidc.user.DefaultUserInfo;
+import org.springframework.security.oauth2.oidc.user.UserInfo;
+
+import java.util.Map;
+
+/**
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ */
+public final class UserInfoConverter extends AbstractOAuth2UserConverter {
+
+ @Override
+ protected UserInfo convert(Map userAttributes) {
+ return new DefaultUserInfo(userAttributes);
+ }
+}
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/nimbus/NimbusClientHttpResponse.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/nimbus/NimbusClientHttpResponse.java
new file mode 100644
index 0000000000..adac4c8a68
--- /dev/null
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/nimbus/NimbusClientHttpResponse.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.user.nimbus;
+
+import com.nimbusds.oauth2.sdk.http.HTTPResponse;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.client.AbstractClientHttpResponse;
+import org.springframework.util.Assert;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+
+/**
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ */
+final class NimbusClientHttpResponse extends AbstractClientHttpResponse {
+ private final HTTPResponse httpResponse;
+ private final HttpHeaders headers;
+
+ NimbusClientHttpResponse(HTTPResponse httpResponse) {
+ Assert.notNull(httpResponse, "httpResponse cannot be null");
+ this.httpResponse = httpResponse;
+ this.headers = new HttpHeaders();
+ this.headers.setAll(httpResponse.getHeaders());
+ }
+
+ @Override
+ public int getRawStatusCode() throws IOException {
+ return this.httpResponse.getStatusCode();
+ }
+
+ @Override
+ public String getStatusText() throws IOException {
+ return String.valueOf(this.getRawStatusCode());
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public InputStream getBody() throws IOException {
+ InputStream inputStream = new ByteArrayInputStream(
+ this.httpResponse.getContent().getBytes(Charset.forName("UTF-8")));
+ return inputStream;
+ }
+
+ @Override
+ public HttpHeaders getHeaders() {
+ return this.headers;
+ }
+}
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/nimbus/NimbusOAuth2UserService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/nimbus/NimbusOAuth2UserService.java
new file mode 100644
index 0000000000..bb5dbf1f7c
--- /dev/null
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/nimbus/NimbusOAuth2UserService.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.user.nimbus;
+
+import com.nimbusds.oauth2.sdk.ErrorObject;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.http.HTTPRequest;
+import com.nimbusds.oauth2.sdk.http.HTTPResponse;
+import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
+import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse;
+import com.nimbusds.openid.connect.sdk.UserInfoRequest;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.http.MediaType;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.core.AuthenticatedPrincipal;
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.user.OAuth2UserService;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.security.oauth2.oidc.user.UserInfo;
+import org.springframework.util.Assert;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * An implementation of an {@link OAuth2UserService} that uses the Nimbus OAuth 2.0 SDK internally.
+ *
+ *
+ * This implementation uses a Map of Converter's keyed by URI.
+ * The URI represents the UserInfo Endpoint address and the mapped Converter
+ * is capable of converting the UserInfo Response to either an
+ * {@link OAuth2User} (for a standard OAuth 2.0 Provider ) or
+ * {@link UserInfo} (for an OpenID Connect 1.0 Provider ).
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see OAuth2AuthenticationToken
+ * @see AuthenticatedPrincipal
+ * @see OAuth2User
+ * @see UserInfo
+ * @see Converter
+ * @see Nimbus OAuth 2.0 SDK
+ */
+public class NimbusOAuth2UserService implements OAuth2UserService {
+ private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";
+ private final Map> userInfoTypeConverters;
+
+ public NimbusOAuth2UserService(Map> userInfoTypeConverters) {
+ Assert.notEmpty(userInfoTypeConverters, "userInfoTypeConverters cannot be empty");
+ this.userInfoTypeConverters = new HashMap<>(userInfoTypeConverters);
+ }
+
+ @Override
+ public OAuth2User loadUser(OAuth2AuthenticationToken token) throws OAuth2AuthenticationException {
+ OAuth2User user;
+
+ try {
+ ClientRegistration clientRegistration = token.getClientRegistration();
+
+ URI userInfoUri;
+ try {
+ userInfoUri = new URI(clientRegistration.getProviderDetails().getUserInfoUri());
+ } catch (Exception ex) {
+ throw new IllegalArgumentException("An error occurred parsing the userInfo URI: " +
+ clientRegistration.getProviderDetails().getUserInfoUri(), ex);
+ }
+
+ Converter userInfoConverter = this.userInfoTypeConverters.get(userInfoUri);
+ if (userInfoConverter == null) {
+ throw new IllegalArgumentException("There is no available User Info converter for " + userInfoUri.toString());
+ }
+
+ BearerAccessToken accessToken = new BearerAccessToken(token.getAccessToken().getTokenValue());
+
+ // Request the User Info
+ UserInfoRequest userInfoRequest = new UserInfoRequest(userInfoUri, accessToken);
+ HTTPRequest httpRequest = userInfoRequest.toHTTPRequest();
+ httpRequest.setAccept(MediaType.APPLICATION_JSON_VALUE);
+ HTTPResponse httpResponse = httpRequest.send();
+
+ if (httpResponse.getStatusCode() != HTTPResponse.SC_OK) {
+ UserInfoErrorResponse userInfoErrorResponse = UserInfoErrorResponse.parse(httpResponse);
+ ErrorObject errorObject = userInfoErrorResponse.getErrorObject();
+ OAuth2Error oauth2Error = new OAuth2Error(errorObject.getCode(), errorObject.getDescription(),
+ (errorObject.getURI() != null ? errorObject.getURI().toString() : null));
+ throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
+ }
+
+ user = userInfoConverter.convert(new NimbusClientHttpResponse(httpResponse));
+
+ } catch (ParseException ex) {
+ // This error occurs if the User Info Response is not well-formed or invalid
+ throw new OAuth2AuthenticationException(new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE), ex);
+ } catch (IOException ex) {
+ // This error occurs when there is a network-related issue
+ throw new AuthenticationServiceException("An error occurred while sending the User Info Request: " +
+ ex.getMessage(), ex);
+ }
+
+ return user;
+ }
+}
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/converter/AuthorizationCodeAuthorizationResponseAttributesConverter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/converter/AuthorizationCodeAuthorizationResponseAttributesConverter.java
new file mode 100644
index 0000000000..9a8fadc9fb
--- /dev/null
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/converter/AuthorizationCodeAuthorizationResponseAttributesConverter.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.web.converter;
+
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.security.oauth2.core.endpoint.OAuth2Parameter;
+import org.springframework.security.oauth2.core.endpoint.AuthorizationCodeAuthorizationResponseAttributes;
+import org.springframework.util.Assert;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ */
+public final class AuthorizationCodeAuthorizationResponseAttributesConverter implements Converter {
+
+ @Override
+ public AuthorizationCodeAuthorizationResponseAttributes convert(HttpServletRequest request) {
+ AuthorizationCodeAuthorizationResponseAttributes response;
+
+ String code = request.getParameter(OAuth2Parameter.CODE);
+ Assert.hasText(code, OAuth2Parameter.CODE + " attribute is required");
+
+ String state = request.getParameter(OAuth2Parameter.STATE);
+
+ response = new AuthorizationCodeAuthorizationResponseAttributes(code, state);
+
+ return response;
+ }
+}
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/converter/ErrorResponseAttributesConverter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/converter/ErrorResponseAttributesConverter.java
new file mode 100644
index 0000000000..2c422e8246
--- /dev/null
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/converter/ErrorResponseAttributesConverter.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.web.converter;
+
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.security.oauth2.core.endpoint.ErrorResponseAttributes;
+import org.springframework.security.oauth2.core.endpoint.OAuth2Parameter;
+import org.springframework.util.StringUtils;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ */
+public final class ErrorResponseAttributesConverter implements Converter {
+
+ @Override
+ public ErrorResponseAttributes convert(HttpServletRequest request) {
+ ErrorResponseAttributes response;
+
+ String errorCode = request.getParameter(OAuth2Parameter.ERROR);
+ if (!StringUtils.hasText(errorCode)) {
+ return null;
+ }
+
+ String description = request.getParameter(OAuth2Parameter.ERROR_DESCRIPTION);
+ String uri = request.getParameter(OAuth2Parameter.ERROR_URI);
+ String state = request.getParameter(OAuth2Parameter.STATE);
+
+ response = ErrorResponseAttributes.withErrorCode(errorCode)
+ .description(description)
+ .uri(uri)
+ .state(state)
+ .build();
+
+ return response;
+ }
+}
diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProcessingFilterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProcessingFilterTests.java
new file mode 100644
index 0000000000..f7cd49b88e
--- /dev/null
+++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProcessingFilterTests.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.authentication;
+
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.endpoint.AuthorizationRequestAttributes;
+import org.springframework.security.oauth2.core.endpoint.OAuth2Parameter;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+
+import javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.*;
+import static org.springframework.security.oauth2.client.authentication.TestUtil.*;
+
+/**
+ * Tests {@link AuthorizationCodeAuthenticationProcessingFilter}.
+ *
+ * @author Joe Grandja
+ */
+public class AuthorizationCodeAuthenticationProcessingFilterTests {
+
+ @Test
+ public void doFilterWhenNotAuthorizationCodeResponseThenContinueChain() throws Exception {
+ ClientRegistration clientRegistration = googleClientRegistration();
+
+ AuthorizationCodeAuthenticationProcessingFilter filter = spy(setupFilter(clientRegistration));
+
+ String requestURI = "/path";
+ MockHttpServletRequest request = new MockHttpServletRequest("GET", requestURI);
+ request.setServletPath(requestURI);
+ MockHttpServletResponse response = new MockHttpServletResponse();
+ FilterChain filterChain = mock(FilterChain.class);
+
+ filter.doFilter(request, response, filterChain);
+
+ verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
+ verify(filter, never()).attemptAuthentication(any(HttpServletRequest.class), any(HttpServletResponse.class));
+ }
+
+ @Test
+ public void doFilterWhenAuthorizationCodeErrorResponseThenAuthenticationFailureHandlerIsCalled() throws Exception {
+ ClientRegistration clientRegistration = githubClientRegistration();
+
+ AuthorizationCodeAuthenticationProcessingFilter filter = spy(setupFilter(clientRegistration));
+ AuthenticationFailureHandler failureHandler = mock(AuthenticationFailureHandler.class);
+ filter.setAuthenticationFailureHandler(failureHandler);
+
+ MockHttpServletRequest request = this.setupRequest(clientRegistration);
+ String errorCode = OAuth2Error.INVALID_GRANT_ERROR_CODE;
+ request.addParameter(OAuth2Parameter.ERROR, errorCode);
+ request.addParameter(OAuth2Parameter.STATE, "some state");
+ MockHttpServletResponse response = new MockHttpServletResponse();
+ FilterChain filterChain = mock(FilterChain.class);
+
+ filter.doFilter(request, response, filterChain);
+
+ verify(filter).attemptAuthentication(any(HttpServletRequest.class), any(HttpServletResponse.class));
+ verify(failureHandler).onAuthenticationFailure(any(HttpServletRequest.class), any(HttpServletResponse.class),
+ any(AuthenticationException.class));
+ }
+
+ @Test
+ public void doFilterWhenAuthorizationCodeSuccessResponseThenAuthenticationSuccessHandlerIsCalled() throws Exception {
+ TestingAuthenticationToken authentication = new TestingAuthenticationToken("joe", "password", "user", "admin");
+ AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
+ when(authenticationManager.authenticate(any(Authentication.class))).thenReturn(authentication);
+
+ ClientRegistration clientRegistration = githubClientRegistration();
+
+ AuthorizationCodeAuthenticationProcessingFilter filter = spy(setupFilter(authenticationManager, clientRegistration));
+ AuthenticationSuccessHandler successHandler = mock(AuthenticationSuccessHandler.class);
+ filter.setAuthenticationSuccessHandler(successHandler);
+ AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionAuthorizationRequestRepository();
+ filter.setAuthorizationRequestRepository(authorizationRequestRepository);
+
+ MockHttpServletRequest request = this.setupRequest(clientRegistration);
+ String authCode = "some code";
+ String state = "some state";
+ request.addParameter(OAuth2Parameter.CODE, authCode);
+ request.addParameter(OAuth2Parameter.STATE, state);
+ setupAuthorizationRequest(authorizationRequestRepository, request, clientRegistration, state);
+ MockHttpServletResponse response = new MockHttpServletResponse();
+ FilterChain filterChain = mock(FilterChain.class);
+
+ filter.doFilter(request, response, filterChain);
+
+ verify(filter).attemptAuthentication(any(HttpServletRequest.class), any(HttpServletResponse.class));
+
+ ArgumentCaptor authenticationArgCaptor = ArgumentCaptor.forClass(Authentication.class);
+ verify(successHandler).onAuthenticationSuccess(any(HttpServletRequest.class), any(HttpServletResponse.class),
+ authenticationArgCaptor.capture());
+ assertThat(authenticationArgCaptor.getValue()).isEqualTo(authentication);
+ }
+
+ @Test
+ public void doFilterWhenAuthorizationCodeSuccessResponseAndNoMatchingAuthorizationRequestThenThrowOAuth2AuthenticationExceptionAuthorizationRequestNotFound() throws Exception {
+ ClientRegistration clientRegistration = githubClientRegistration();
+
+ AuthorizationCodeAuthenticationProcessingFilter filter = spy(setupFilter(clientRegistration));
+ AuthenticationFailureHandler failureHandler = mock(AuthenticationFailureHandler.class);
+ filter.setAuthenticationFailureHandler(failureHandler);
+
+ MockHttpServletRequest request = this.setupRequest(clientRegistration);
+ String authCode = "some code";
+ String state = "some state";
+ request.addParameter(OAuth2Parameter.CODE, authCode);
+ request.addParameter(OAuth2Parameter.STATE, state);
+ MockHttpServletResponse response = new MockHttpServletResponse();
+ FilterChain filterChain = mock(FilterChain.class);
+
+ filter.doFilter(request, response, filterChain);
+
+ verifyThrowsOAuth2AuthenticationExceptionWithErrorCode(filter, failureHandler, "authorization_request_not_found");
+ }
+
+ @Test
+ public void doFilterWhenAuthorizationCodeSuccessResponseWithInvalidStateParamThenThrowOAuth2AuthenticationExceptionInvalidStateParameter() throws Exception {
+ ClientRegistration clientRegistration = githubClientRegistration();
+
+ AuthorizationCodeAuthenticationProcessingFilter filter = spy(setupFilter(clientRegistration));
+ AuthenticationFailureHandler failureHandler = mock(AuthenticationFailureHandler.class);
+ filter.setAuthenticationFailureHandler(failureHandler);
+ AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionAuthorizationRequestRepository();
+ filter.setAuthorizationRequestRepository(authorizationRequestRepository);
+
+ MockHttpServletRequest request = this.setupRequest(clientRegistration);
+ String authCode = "some code";
+ String state = "some other state";
+ request.addParameter(OAuth2Parameter.CODE, authCode);
+ request.addParameter(OAuth2Parameter.STATE, state);
+ setupAuthorizationRequest(authorizationRequestRepository, request, clientRegistration, "some state");
+ MockHttpServletResponse response = new MockHttpServletResponse();
+ FilterChain filterChain = mock(FilterChain.class);
+
+ filter.doFilter(request, response, filterChain);
+
+ verifyThrowsOAuth2AuthenticationExceptionWithErrorCode(filter, failureHandler, "invalid_state_parameter");
+ }
+
+ @Test
+ public void doFilterWhenAuthorizationCodeSuccessResponseWithInvalidRedirectUriParamThenThrowOAuth2AuthenticationExceptionInvalidRedirectUriParameter() throws Exception {
+ ClientRegistration clientRegistration = githubClientRegistration();
+
+ AuthorizationCodeAuthenticationProcessingFilter filter = spy(setupFilter(clientRegistration));
+ AuthenticationFailureHandler failureHandler = mock(AuthenticationFailureHandler.class);
+ filter.setAuthenticationFailureHandler(failureHandler);
+ AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionAuthorizationRequestRepository();
+ filter.setAuthorizationRequestRepository(authorizationRequestRepository);
+
+ MockHttpServletRequest request = this.setupRequest(clientRegistration);
+ request.setRequestURI(request.getRequestURI() + "-other");
+ String authCode = "some code";
+ String state = "some state";
+ request.addParameter(OAuth2Parameter.CODE, authCode);
+ request.addParameter(OAuth2Parameter.STATE, state);
+ setupAuthorizationRequest(authorizationRequestRepository, request, clientRegistration, state);
+ MockHttpServletResponse response = new MockHttpServletResponse();
+ FilterChain filterChain = mock(FilterChain.class);
+
+ filter.doFilter(request, response, filterChain);
+
+ verifyThrowsOAuth2AuthenticationExceptionWithErrorCode(filter, failureHandler, "invalid_redirect_uri_parameter");
+ }
+
+ private void verifyThrowsOAuth2AuthenticationExceptionWithErrorCode(AuthorizationCodeAuthenticationProcessingFilter filter,
+ AuthenticationFailureHandler failureHandler,
+ String errorCode) throws Exception {
+
+ verify(filter).attemptAuthentication(any(HttpServletRequest.class), any(HttpServletResponse.class));
+
+ ArgumentCaptor authenticationExceptionArgCaptor =
+ ArgumentCaptor.forClass(AuthenticationException.class);
+ verify(failureHandler).onAuthenticationFailure(any(HttpServletRequest.class), any(HttpServletResponse.class),
+ authenticationExceptionArgCaptor.capture());
+ assertThat(authenticationExceptionArgCaptor.getValue()).isInstanceOf(OAuth2AuthenticationException.class);
+ OAuth2AuthenticationException oauth2AuthenticationException =
+ (OAuth2AuthenticationException)authenticationExceptionArgCaptor.getValue();
+ assertThat(oauth2AuthenticationException.getErrorObject()).isNotNull();
+ assertThat(oauth2AuthenticationException.getErrorObject().getErrorCode()).isEqualTo(errorCode);
+ }
+
+ private AuthorizationCodeAuthenticationProcessingFilter setupFilter(ClientRegistration... clientRegistrations) throws Exception {
+ AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
+
+ return setupFilter(authenticationManager, clientRegistrations);
+ }
+
+ private AuthorizationCodeAuthenticationProcessingFilter setupFilter(
+ AuthenticationManager authenticationManager, ClientRegistration... clientRegistrations) throws Exception {
+
+ ClientRegistrationRepository clientRegistrationRepository = clientRegistrationRepository(clientRegistrations);
+
+ AuthorizationCodeAuthenticationProcessingFilter filter = new AuthorizationCodeAuthenticationProcessingFilter();
+ filter.setClientRegistrationRepository(clientRegistrationRepository);
+ filter.setAuthenticationManager(authenticationManager);
+
+ return filter;
+ }
+
+ private void setupAuthorizationRequest(AuthorizationRequestRepository authorizationRequestRepository,
+ HttpServletRequest request,
+ ClientRegistration clientRegistration,
+ String state) {
+
+ AuthorizationRequestAttributes authorizationRequestAttributes =
+ AuthorizationRequestAttributes.withAuthorizationCode()
+ .clientId(clientRegistration.getClientId())
+ .authorizeUri(clientRegistration.getProviderDetails().getAuthorizationUri())
+ .redirectUri(clientRegistration.getRedirectUri())
+ .scopes(clientRegistration.getScopes())
+ .state(state)
+ .build();
+
+ authorizationRequestRepository.saveAuthorizationRequest(authorizationRequestAttributes, request);
+ }
+
+ private MockHttpServletRequest setupRequest(ClientRegistration clientRegistration) {
+ String requestURI = AUTHORIZE_BASE_URI + "/" + clientRegistration.getClientAlias();
+ MockHttpServletRequest request = new MockHttpServletRequest("GET", requestURI);
+ request.setScheme(DEFAULT_SCHEME);
+ request.setServerName(DEFAULT_SERVER_NAME);
+ request.setServerPort(DEFAULT_SERVER_PORT);
+ request.setServletPath(requestURI);
+ return request;
+ }
+}
diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeRequestRedirectFilterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeRequestRedirectFilterTests.java
new file mode 100644
index 0000000000..cf68361cf3
--- /dev/null
+++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeRequestRedirectFilterTests.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.authentication;
+
+import org.junit.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import org.springframework.security.oauth2.core.endpoint.AuthorizationRequestAttributes;
+
+import javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.net.URI;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.*;
+import static org.springframework.security.oauth2.client.authentication.TestUtil.*;
+
+/**
+ * Tests {@link AuthorizationCodeRequestRedirectFilter}.
+ *
+ * @author Joe Grandja
+ */
+public class AuthorizationCodeRequestRedirectFilterTests {
+
+ @Test(expected = IllegalArgumentException.class)
+ public void constructorWhenClientRegistrationRepositoryIsNullThenThrowIllegalArgumentException() {
+ new AuthorizationCodeRequestRedirectFilter(null, mock(AuthorizationRequestUriBuilder.class));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void constructorWhenAuthorizationRequestUriBuilderIsNullThenThrowIllegalArgumentException() {
+ new AuthorizationCodeRequestRedirectFilter(mock(ClientRegistrationRepository.class), null);
+ }
+
+ @Test
+ public void doFilterWhenRequestDoesNotMatchClientThenContinueChain() throws Exception {
+ ClientRegistration clientRegistration = googleClientRegistration();
+ String authorizationUri = clientRegistration.getProviderDetails().getAuthorizationUri().toString();
+ AuthorizationCodeRequestRedirectFilter filter =
+ setupFilter(authorizationUri, clientRegistration);
+
+ String requestURI = "/path";
+ MockHttpServletRequest request = new MockHttpServletRequest("GET", requestURI);
+ request.setServletPath(requestURI);
+ MockHttpServletResponse response = new MockHttpServletResponse();
+ FilterChain filterChain = mock(FilterChain.class);
+
+ filter.doFilter(request, response, filterChain);
+
+ verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
+ }
+
+ @Test
+ public void doFilterWhenRequestMatchesClientThenRedirectForAuthorization() throws Exception {
+ ClientRegistration clientRegistration = googleClientRegistration();
+ String authorizationUri = clientRegistration.getProviderDetails().getAuthorizationUri().toString();
+ AuthorizationCodeRequestRedirectFilter filter =
+ setupFilter(authorizationUri, clientRegistration);
+
+ String requestUri = AUTHORIZATION_BASE_URI + "/" + clientRegistration.getClientAlias();
+ MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
+ request.setServletPath(requestUri);
+ MockHttpServletResponse response = new MockHttpServletResponse();
+ FilterChain filterChain = mock(FilterChain.class);
+
+ filter.doFilter(request, response, filterChain);
+
+ verifyZeroInteractions(filterChain); // Request should not proceed up the chain
+
+ assertThat(response.getRedirectedUrl()).isEqualTo(authorizationUri);
+ }
+
+ @Test
+ public void doFilterWhenRequestMatchesClientThenAuthorizationRequestSavedInSession() throws Exception {
+ ClientRegistration clientRegistration = githubClientRegistration();
+ String authorizationUri = clientRegistration.getProviderDetails().getAuthorizationUri().toString();
+ AuthorizationCodeRequestRedirectFilter filter =
+ setupFilter(authorizationUri, clientRegistration);
+ AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionAuthorizationRequestRepository();
+ filter.setAuthorizationRequestRepository(authorizationRequestRepository);
+
+ String requestUri = AUTHORIZATION_BASE_URI + "/" + clientRegistration.getClientAlias();
+ MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
+ request.setServletPath(requestUri);
+ MockHttpServletResponse response = new MockHttpServletResponse();
+ FilterChain filterChain = mock(FilterChain.class);
+
+ filter.doFilter(request, response, filterChain);
+
+ verifyZeroInteractions(filterChain); // Request should not proceed up the chain
+
+ // The authorization request attributes are saved in the session before the redirect happens
+ AuthorizationRequestAttributes authorizationRequestAttributes =
+ authorizationRequestRepository.loadAuthorizationRequest(request);
+ assertThat(authorizationRequestAttributes).isNotNull();
+
+ assertThat(authorizationRequestAttributes.getAuthorizeUri()).isNotNull();
+ assertThat(authorizationRequestAttributes.getGrantType()).isNotNull();
+ assertThat(authorizationRequestAttributes.getResponseType()).isNotNull();
+ assertThat(authorizationRequestAttributes.getClientId()).isNotNull();
+ assertThat(authorizationRequestAttributes.getRedirectUri()).isNotNull();
+ assertThat(authorizationRequestAttributes.getScopes()).isNotNull();
+ assertThat(authorizationRequestAttributes.getState()).isNotNull();
+ }
+
+ private AuthorizationCodeRequestRedirectFilter setupFilter(String authorizationUri,
+ ClientRegistration... clientRegistrations) throws Exception {
+
+ AuthorizationRequestUriBuilder authorizationUriBuilder = mock(AuthorizationRequestUriBuilder.class);
+ URI authorizationURI = new URI(authorizationUri);
+ when(authorizationUriBuilder.build(any(AuthorizationRequestAttributes.class))).thenReturn(authorizationURI);
+
+ return setupFilter(authorizationUriBuilder, clientRegistrations);
+ }
+
+ private AuthorizationCodeRequestRedirectFilter setupFilter(AuthorizationRequestUriBuilder authorizationUriBuilder,
+ ClientRegistration... clientRegistrations) throws Exception {
+
+ ClientRegistrationRepository clientRegistrationRepository = clientRegistrationRepository(clientRegistrations);
+
+ AuthorizationCodeRequestRedirectFilter filter = new AuthorizationCodeRequestRedirectFilter(
+ clientRegistrationRepository, authorizationUriBuilder);
+
+ return filter;
+ }
+}
diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/TestUtil.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/TestUtil.java
new file mode 100644
index 0000000000..5d6fe081bb
--- /dev/null
+++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/TestUtil.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.client.authentication;
+
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationProperties;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+/**
+ * @author Joe Grandja
+ */
+class TestUtil {
+ static final String DEFAULT_SCHEME = "https";
+ static final String DEFAULT_SERVER_NAME = "localhost";
+ static final int DEFAULT_SERVER_PORT = 8080;
+ static final String DEFAULT_SERVER_URL = DEFAULT_SCHEME + "://" + DEFAULT_SERVER_NAME + ":" + DEFAULT_SERVER_PORT;
+ static final String AUTHORIZATION_BASE_URI = "/oauth2/authorization/code";
+ static final String AUTHORIZE_BASE_URI = "/oauth2/authorize/code";
+ static final String GOOGLE_CLIENT_ALIAS = "google";
+ static final String GITHUB_CLIENT_ALIAS = "github";
+
+ static ClientRegistrationRepository clientRegistrationRepository(ClientRegistration... clientRegistrations) {
+ return new InMemoryClientRegistrationRepository(Arrays.asList(clientRegistrations));
+ }
+
+ static ClientRegistration googleClientRegistration() {
+ return googleClientRegistration(DEFAULT_SERVER_URL + AUTHORIZE_BASE_URI + "/" + GOOGLE_CLIENT_ALIAS);
+ }
+
+ static ClientRegistration googleClientRegistration(String redirectUri) {
+ ClientRegistrationProperties clientRegistrationProperties = new ClientRegistrationProperties();
+ clientRegistrationProperties.setClientId("google-client-id");
+ clientRegistrationProperties.setClientSecret("secret");
+ clientRegistrationProperties.setAuthorizedGrantType(AuthorizationGrantType.AUTHORIZATION_CODE);
+ clientRegistrationProperties.setClientName("Google Client");
+ clientRegistrationProperties.setClientAlias(GOOGLE_CLIENT_ALIAS);
+ clientRegistrationProperties.setAuthorizationUri("https://accounts.google.com/o/oauth2/auth");
+ clientRegistrationProperties.setTokenUri("https://accounts.google.com/o/oauth2/token");
+ clientRegistrationProperties.setUserInfoUri("https://www.googleapis.com/oauth2/v3/userinfo");
+ clientRegistrationProperties.setRedirectUri(redirectUri);
+ clientRegistrationProperties.setScopes(Arrays.stream(new String[] {"openid", "email", "profile"}).collect(Collectors.toSet()));
+ return new ClientRegistration.Builder(clientRegistrationProperties).build();
+ }
+
+ static ClientRegistration githubClientRegistration() {
+ return githubClientRegistration(DEFAULT_SERVER_URL + AUTHORIZE_BASE_URI + "/" + GITHUB_CLIENT_ALIAS);
+ }
+
+ static ClientRegistration githubClientRegistration(String redirectUri) {
+ ClientRegistrationProperties clientRegistrationProperties = new ClientRegistrationProperties();
+ clientRegistrationProperties.setClientId("github-client-id");
+ clientRegistrationProperties.setClientSecret("secret");
+ clientRegistrationProperties.setAuthorizedGrantType(AuthorizationGrantType.AUTHORIZATION_CODE);
+ clientRegistrationProperties.setClientName("GitHub Client");
+ clientRegistrationProperties.setClientAlias(GITHUB_CLIENT_ALIAS);
+ clientRegistrationProperties.setAuthorizationUri("https://github.com/login/oauth/authorize");
+ clientRegistrationProperties.setTokenUri("https://github.com/login/oauth/access_token");
+ clientRegistrationProperties.setUserInfoUri("https://api.github.com/user");
+ clientRegistrationProperties.setRedirectUri(redirectUri);
+ clientRegistrationProperties.setScopes(Arrays.stream(new String[] {"user"}).collect(Collectors.toSet()));
+ return new ClientRegistration.Builder(clientRegistrationProperties).build();
+ }
+}
diff --git a/oauth2/oauth2-core/pom.xml b/oauth2/oauth2-core/pom.xml
new file mode 100644
index 0000000000..bf82370f24
--- /dev/null
+++ b/oauth2/oauth2-core/pom.xml
@@ -0,0 +1,138 @@
+
+
+ 4.0.0
+ org.springframework.security
+ spring-security-oauth2-core
+ 5.0.0.BUILD-SNAPSHOT
+ spring-security-oauth2-core
+ spring-security-oauth2-core
+ http://spring.io/spring-security
+
+ spring.io
+ http://spring.io/
+
+
+
+ The Apache Software License, Version 2.0
+ http://www.apache.org/licenses/LICENSE-2.0.txt
+ repo
+
+
+
+
+ rwinch
+ Rob Winch
+ rwinch@pivotal.io
+
+
+ jgrandja
+ Joe Grandja
+ jgrandja@pivotal.io
+
+
+
+ scm:git:git://github.com/spring-projects/spring-security
+ scm:git:git://github.com/spring-projects/spring-security
+ https://github.com/spring-projects/spring-security
+
+
+
+
+ org.springframework
+ spring-framework-bom
+ 4.3.5.RELEASE
+ pom
+ import
+
+
+
+
+
+ org.springframework.security
+ spring-security-core
+ 5.0.0.BUILD-SNAPSHOT
+ compile
+
+
+ org.springframework
+ spring-core
+ compile
+
+
+ commons-logging
+ commons-logging
+
+
+
+
+ org.springframework
+ spring-web
+ compile
+
+
+ commons-logging
+ commons-logging
+ 1.2
+ compile
+ true
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+ ch.qos.logback
+ logback-classic
+ 1.1.2
+ test
+
+
+ junit
+ junit
+ 4.12
+ test
+
+
+ org.assertj
+ assertj-core
+ 3.6.2
+ test
+
+
+ org.mockito
+ mockito-core
+ 1.10.19
+ test
+
+
+ org.slf4j
+ jcl-over-slf4j
+ 1.7.7
+ test
+
+
+ org.springframework
+ spring-test
+ test
+
+
+
+
+ spring-snapshot
+ https://repo.spring.io/snapshot
+
+
+
+
+
+ maven-compiler-plugin
+
+ 1.8
+ 1.8
+
+
+
+
+
diff --git a/oauth2/oauth2-core/spring-security-oauth2-core.gradle b/oauth2/oauth2-core/spring-security-oauth2-core.gradle
new file mode 100644
index 0000000000..83ea464e63
--- /dev/null
+++ b/oauth2/oauth2-core/spring-security-oauth2-core.gradle
@@ -0,0 +1,9 @@
+apply plugin: 'io.spring.convention.spring-module'
+
+dependencies {
+ compile project(':spring-security-core')
+ compile springCoreDependency
+ compile 'org.springframework:spring-web'
+
+ provided 'javax.servlet:javax.servlet-api'
+}
diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AbstractToken.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AbstractToken.java
new file mode 100644
index 0000000000..39fca83d6e
--- /dev/null
+++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AbstractToken.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.core;
+
+import org.springframework.security.core.SpringSecurityCoreVersion;
+import org.springframework.util.Assert;
+
+import java.io.Serializable;
+import java.time.Instant;
+
+/**
+ * Base class for Security Token implementations.
+ *
+ *
+ * It is highly recommended that implementations be immutable.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ */
+public abstract class AbstractToken implements Serializable {
+ private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
+ private final String tokenValue;
+ private final Instant issuedAt;
+ private final Instant expiresAt;
+
+ protected AbstractToken(String tokenValue, Instant issuedAt, Instant expiresAt) {
+ Assert.hasLength(tokenValue, "tokenValue cannot be empty");
+ Assert.notNull(issuedAt, "issuedAt cannot be null");
+ Assert.notNull(expiresAt, "expiresAt cannot be null");
+ this.tokenValue = tokenValue;
+ this.issuedAt = issuedAt;
+ this.expiresAt = expiresAt;
+ }
+
+ public String getTokenValue() {
+ return this.tokenValue;
+ }
+
+ public Instant getIssuedAt() {
+ return this.issuedAt;
+ }
+
+ public Instant getExpiresAt() {
+ return this.expiresAt;
+ }
+}
diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AccessToken.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AccessToken.java
new file mode 100644
index 0000000000..097c3a0c22
--- /dev/null
+++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AccessToken.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.core;
+
+import org.springframework.util.Assert;
+
+import java.time.Instant;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * An implementation of an {@link AbstractToken} representing an OAuth 2.0 Access Token .
+ *
+ *
+ * An access token is a credential that represents an authorization
+ * granted by the resource owner to the client.
+ * It is primarily used by the client to access protected resources on either a
+ * resource server or the authorization server that originally issued the access token.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see Section 1.4 Access Token
+ */
+public class AccessToken extends AbstractToken {
+ private final TokenType tokenType;
+ private final Set scopes;
+ private final Map additionalParameters;
+
+ public enum TokenType {
+ BEARER("Bearer");
+
+ private final String value;
+
+ TokenType(String value) {
+ this.value = value;
+ }
+
+ public String value() {
+ return this.value;
+ }
+ }
+
+ public AccessToken(TokenType tokenType, String tokenValue, Instant issuedAt, Instant expiresAt) {
+ this(tokenType, tokenValue, issuedAt, expiresAt, Collections.emptySet());
+ }
+
+ public AccessToken(TokenType tokenType, String tokenValue, Instant issuedAt, Instant expiresAt, Set scopes) {
+ this(tokenType, tokenValue, issuedAt, expiresAt, scopes, Collections.emptyMap());
+ }
+
+ public AccessToken(TokenType tokenType, String tokenValue, Instant issuedAt, Instant expiresAt,
+ Set scopes, Map additionalParameters) {
+
+ super(tokenValue, issuedAt, expiresAt);
+ Assert.notNull(tokenType, "tokenType cannot be null");
+ this.tokenType = tokenType;
+ this.scopes = Collections.unmodifiableSet(
+ scopes != null ? scopes : Collections.emptySet());
+ this.additionalParameters = Collections.unmodifiableMap(
+ additionalParameters != null ? additionalParameters : Collections.emptyMap());
+ }
+
+ public TokenType getTokenType() {
+ return this.tokenType;
+ }
+
+ public Set getScopes() {
+ return this.scopes;
+ }
+
+ public Map getAdditionalParameters() {
+ return additionalParameters;
+ }
+}
diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java
new file mode 100644
index 0000000000..dc36331720
--- /dev/null
+++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.core;
+
+/**
+ * An authorization grant is a credential representing the resource owner's authorization
+ * (to access it's protected resources) to the client and used by the client to obtain an access token.
+ *
+ *
+ * The OAuth 2.0 Authorization Framework defines four standard grant types:
+ * authorization code, implicit, resource owner password credentials, and client credentials.
+ * It also provides an extensibility mechanism for defining additional grant types.
+ *
+ *
+ * NOTE: "authorization code" is currently the only supported grant type.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see Section 1.3 Authorization Grant
+ */
+public enum AuthorizationGrantType {
+ AUTHORIZATION_CODE("authorization_code");
+
+ private final String value;
+
+ AuthorizationGrantType(String value) {
+ this.value = value;
+ }
+
+ public String value() {
+ return this.value;
+ }
+}
diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClientAuthenticationMethod.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClientAuthenticationMethod.java
new file mode 100644
index 0000000000..19a3bf1031
--- /dev/null
+++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClientAuthenticationMethod.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.core;
+
+/**
+ * The available authentication methods used when authenticating the client with the authorization server.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see Section 2.3 Client Authentication
+ */
+public enum ClientAuthenticationMethod {
+ HEADER("header"),
+ FORM("form");
+
+ private final String value;
+
+ ClientAuthenticationMethod(String value) {
+ this.value = value;
+ }
+
+ public String value() {
+ return this.value;
+ }
+}
diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2Error.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2Error.java
new file mode 100644
index 0000000000..39841a9fcf
--- /dev/null
+++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2Error.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.core;
+
+import org.springframework.util.Assert;
+
+/**
+ * A representation of an OAuth 2.0 Error .
+ *
+ *
+ * At a minimum, an error response will contain an error code.
+ * The error code may be one of the standard codes defined by the specification,
+ * or a new code defined in the OAuth Extensions Error Registry ,
+ * for cases where protocol extensions require additional error code(s) above the standard codes.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see Section 11.4 OAuth Extensions Error Registry
+ */
+public class OAuth2Error {
+ // Standard error codes
+ public static final String INVALID_REQUEST_ERROR_CODE = "invalid_request";
+ public static final String INVALID_CLIENT_ERROR_CODE = "invalid_client";
+ public static final String INVALID_GRANT_ERROR_CODE = "invalid_grant";
+ public static final String UNAUTHORIZED_CLIENT_ERROR_CODE = "unauthorized_client";
+ public static final String UNSUPPORTED_GRANT_TYPE_ERROR_CODE = "unsupported_grant_type";
+ public static final String INVALID_SCOPE_ERROR_CODE = "invalid_scope";
+
+ private final String errorCode;
+ private final String description;
+ private final String uri;
+
+ public OAuth2Error(String errorCode) {
+ this(errorCode, null, null);
+ }
+
+ public OAuth2Error(String errorCode, String description, String uri) {
+ Assert.hasText(errorCode, "errorCode cannot be empty");
+ this.errorCode = errorCode;
+ this.description = description;
+ this.uri = uri;
+ }
+
+ public String getErrorCode() {
+ return this.errorCode;
+ }
+
+ public String getDescription() {
+ return this.description;
+ }
+
+ public String getUri() {
+ return this.uri;
+ }
+
+ @Override
+ public String toString() {
+ return "[" + this.getErrorCode() + "] " +
+ (this.getDescription() != null ? this.getDescription() : "");
+ }
+}
diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/AuthorizationCodeAuthorizationResponseAttributes.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/AuthorizationCodeAuthorizationResponseAttributes.java
new file mode 100644
index 0000000000..707fd78c9d
--- /dev/null
+++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/AuthorizationCodeAuthorizationResponseAttributes.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.core.endpoint;
+
+import org.springframework.util.Assert;
+
+/**
+ * A representation of an OAuth 2.0 Authorization Response for the authorization code grant type.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see Section 4.1.2 Authorization Response
+ */
+public final class AuthorizationCodeAuthorizationResponseAttributes {
+ private final String code;
+ private final String state;
+
+ public AuthorizationCodeAuthorizationResponseAttributes(String code, String state) {
+ Assert.notNull(code, "code cannot be null");
+ this.code = code;
+ this.state = state;
+ }
+
+ public String getCode() {
+ return this.code;
+ }
+
+ public String getState() {
+ return this.state;
+ }
+}
diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/AuthorizationCodeTokenRequestAttributes.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/AuthorizationCodeTokenRequestAttributes.java
new file mode 100644
index 0000000000..ce8443baaf
--- /dev/null
+++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/AuthorizationCodeTokenRequestAttributes.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.core.endpoint;
+
+import org.springframework.util.Assert;
+
+/**
+ * A representation of an OAuth 2.0 Access Token Request for the authorization code grant type.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see Section 4.1.3 Access Token Request
+ */
+public final class AuthorizationCodeTokenRequestAttributes {
+ private String code;
+ private String clientId;
+ private String redirectUri;
+
+ private AuthorizationCodeTokenRequestAttributes() {
+ }
+
+ public String getCode() {
+ return this.code;
+ }
+
+ public String getClientId() {
+ return this.clientId;
+ }
+
+ public String getRedirectUri() {
+ return this.redirectUri;
+ }
+
+ public static Builder withCode(String code) {
+ return new Builder(code);
+ }
+
+ public static class Builder {
+ private final AuthorizationCodeTokenRequestAttributes authorizationCodeTokenRequest;
+
+ private Builder(String code) {
+ Assert.hasText(code, "code cannot be empty");
+ this.authorizationCodeTokenRequest = new AuthorizationCodeTokenRequestAttributes();
+ this.authorizationCodeTokenRequest.code = code;
+ }
+
+ public Builder clientId(String clientId) {
+ Assert.hasText(clientId, "clientId cannot be empty");
+ this.authorizationCodeTokenRequest.clientId = clientId;
+ return this;
+ }
+
+ public Builder redirectUri(String redirectUri) {
+ Assert.hasText(redirectUri, "redirectUri cannot be empty");
+ this.authorizationCodeTokenRequest.redirectUri = redirectUri;
+ return this;
+ }
+
+ public AuthorizationCodeTokenRequestAttributes build() {
+ return this.authorizationCodeTokenRequest;
+ }
+ }
+}
diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/AuthorizationRequestAttributes.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/AuthorizationRequestAttributes.java
new file mode 100644
index 0000000000..a4ea4c0e3d
--- /dev/null
+++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/AuthorizationRequestAttributes.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.core.endpoint;
+
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * A representation of an OAuth 2.0 Authorization Request
+ * for the authorization code grant type or implicit grant type.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see AuthorizationGrantType
+ * @see ResponseType
+ * @see Section 4.1.1 Authorization Code Grant Request
+ * @see Section 4.2.1 Implicit Grant Request
+ */
+public final class AuthorizationRequestAttributes implements Serializable {
+ private String authorizeUri;
+ private AuthorizationGrantType authorizationGrantType;
+ private ResponseType responseType;
+ private String clientId;
+ private String redirectUri;
+ private Set scopes;
+ private String state;
+
+ private AuthorizationRequestAttributes() {
+ }
+
+ public String getAuthorizeUri() {
+ return this.authorizeUri;
+ }
+
+ public AuthorizationGrantType getGrantType() {
+ return this.authorizationGrantType;
+ }
+
+ public ResponseType getResponseType() {
+ return this.responseType;
+ }
+
+ public String getClientId() {
+ return this.clientId;
+ }
+
+ public String getRedirectUri() {
+ return this.redirectUri;
+ }
+
+ public Set getScopes() {
+ return this.scopes;
+ }
+
+ public String getState() {
+ return this.state;
+ }
+
+ public static Builder withAuthorizationCode() {
+ return new Builder(AuthorizationGrantType.AUTHORIZATION_CODE);
+ }
+
+ public static class Builder {
+ private final AuthorizationRequestAttributes authorizationRequest;
+
+ private Builder(AuthorizationGrantType authorizationGrantType) {
+ Assert.notNull(authorizationGrantType, "authorizationGrantType cannot be null");
+ this.authorizationRequest = new AuthorizationRequestAttributes();
+ this.authorizationRequest.authorizationGrantType = authorizationGrantType;
+ if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationGrantType)) {
+ this.authorizationRequest.responseType = ResponseType.CODE;
+ }
+ }
+
+ public Builder authorizeUri(String authorizeUri) {
+ Assert.hasText(authorizeUri, "authorizeUri cannot be empty");
+ this.authorizationRequest.authorizeUri = authorizeUri;
+ return this;
+ }
+
+ public Builder clientId(String clientId) {
+ Assert.hasText(clientId, "clientId cannot be empty");
+ this.authorizationRequest.clientId = clientId;
+ return this;
+ }
+
+ public Builder redirectUri(String redirectUri) {
+ Assert.hasText(redirectUri, "redirectUri cannot be empty");
+ this.authorizationRequest.redirectUri = redirectUri;
+ return this;
+ }
+
+ public Builder scopes(Set scopes) {
+ this.authorizationRequest.scopes = Collections.unmodifiableSet(
+ CollectionUtils.isEmpty(scopes) ? Collections.emptySet() : new LinkedHashSet<>(scopes));
+ return this;
+ }
+
+ public Builder state(String state) {
+ this.authorizationRequest.state = state;
+ return this;
+ }
+
+ public AuthorizationRequestAttributes build() {
+ return this.authorizationRequest;
+ }
+ }
+}
diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/ErrorResponseAttributes.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/ErrorResponseAttributes.java
new file mode 100644
index 0000000000..dca297df36
--- /dev/null
+++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/ErrorResponseAttributes.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.core.endpoint;
+
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.util.Assert;
+
+/**
+ * A representation of an OAuth 2.0 Error Response .
+ *
+ *
+ * An error response may be returned from either of the following locations:
+ *
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ */
+public final class ErrorResponseAttributes {
+ private OAuth2Error errorObject;
+ private String state;
+
+ private ErrorResponseAttributes() {
+ }
+
+ public String getErrorCode() {
+ return this.errorObject.getErrorCode();
+ }
+
+ public String getDescription() {
+ return this.errorObject.getDescription();
+ }
+
+ public String getUri() {
+ return this.errorObject.getUri();
+ }
+
+ public String getState() {
+ return this.state;
+ }
+
+ public static Builder withErrorCode(String errorCode) {
+ return new Builder(errorCode);
+ }
+
+ public static class Builder {
+ private String errorCode;
+ private String description;
+ private String uri;
+ private String state;
+
+ private Builder(String errorCode) {
+ Assert.hasText(errorCode, "errorCode cannot be empty");
+ this.errorCode = errorCode;
+ }
+
+ public Builder description(String description) {
+ this.description = description;
+ return this;
+ }
+
+ public Builder uri(String uri) {
+ this.uri = uri;
+ return this;
+ }
+
+ public Builder state(String state) {
+ this.state = state;
+ return this;
+ }
+
+ public ErrorResponseAttributes build() {
+ ErrorResponseAttributes errorResponse = new ErrorResponseAttributes();
+ errorResponse.errorObject = new OAuth2Error(this.errorCode, this.description, this.uri);
+ errorResponse.state = this.state;
+ return errorResponse;
+ }
+ }
+}
diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2Parameter.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2Parameter.java
new file mode 100644
index 0000000000..fd3becc823
--- /dev/null
+++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2Parameter.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.core.endpoint;
+
+/**
+ * Standard parameters defined in the OAuth Parameters Registry
+ * and used by the authorization endpoint and token endpoint.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see 11.2 OAuth Parameters Registry
+ */
+public interface OAuth2Parameter {
+
+ String RESPONSE_TYPE = "response_type";
+
+ String CLIENT_ID = "client_id";
+
+ String REDIRECT_URI = "redirect_uri";
+
+ String SCOPE = "scope";
+
+ String STATE = "state";
+
+ String CODE = "code";
+
+ String ERROR = "error";
+
+ String ERROR_DESCRIPTION = "error_description";
+
+ String ERROR_URI = "error_uri";
+
+}
diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/ResponseType.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/ResponseType.java
new file mode 100644
index 0000000000..c443682442
--- /dev/null
+++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/ResponseType.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.core.endpoint;
+
+/**
+ * The response_type parameter is consumed by the authorization endpoint which
+ * is used by the authorization code grant type and implicit grant type flows.
+ * The client sets the response_type parameter with the desired grant type before initiating the authorization request.
+ *
+ *
+ * The response_type parameter value may be one of "code" for requesting an authorization code or
+ * "token" for requesting an access token (implicit grant).
+
+ *
+ * NOTE: "code" is currently the only supported response type.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see Section 3.1.1 Response Type
+ */
+public enum ResponseType {
+ CODE("code");
+
+ private final String value;
+
+ ResponseType(String value) {
+ this.value = value;
+ }
+
+ public String value() {
+ return this.value;
+ }
+}
diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/TokenResponseAttributes.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/TokenResponseAttributes.java
new file mode 100644
index 0000000000..0743aafdfd
--- /dev/null
+++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/TokenResponseAttributes.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.core.endpoint;
+
+import org.springframework.security.oauth2.core.AccessToken;
+import org.springframework.util.Assert;
+
+import java.time.Instant;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A representation of an OAuth 2.0 Access Token Response .
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see AccessToken
+ * @see Section 5.1 Access Token Response
+ */
+public final class TokenResponseAttributes {
+ private AccessToken accessToken;
+
+ private TokenResponseAttributes() {
+ }
+
+ public String getTokenValue() {
+ return this.accessToken.getTokenValue();
+ }
+
+ public AccessToken.TokenType getTokenType() {
+ return this.accessToken.getTokenType();
+ }
+
+ public Instant getIssuedAt() {
+ return this.accessToken.getIssuedAt();
+ }
+
+ public Instant getExpiresAt() {
+ return this.accessToken.getExpiresAt();
+ }
+
+ public Set getScopes() {
+ return this.accessToken.getScopes();
+ }
+
+ public Map getAdditionalParameters() {
+ return this.accessToken.getAdditionalParameters();
+ }
+
+ public static Builder withToken(String tokenValue) {
+ return new Builder(tokenValue);
+ }
+
+ public static class Builder {
+ private String tokenValue;
+ private AccessToken.TokenType tokenType;
+ private long expiresIn;
+ private Set scopes;
+ private Map additionalParameters;
+
+ private Builder(String tokenValue) {
+ this.tokenValue = tokenValue;
+ }
+
+ public Builder tokenType(AccessToken.TokenType tokenType) {
+ this.tokenType = tokenType;
+ return this;
+ }
+
+ public Builder expiresIn(long expiresIn) {
+ this.expiresIn = expiresIn;
+ return this;
+ }
+
+ public Builder scopes(Set scopes) {
+ this.scopes = scopes;
+ return this;
+ }
+
+ public Builder additionalParameters(Map additionalParameters) {
+ this.additionalParameters = additionalParameters;
+ return this;
+ }
+
+ public TokenResponseAttributes build() {
+ Assert.isTrue(this.expiresIn >= 0, "expiresIn must be a positive number");
+ Instant issuedAt = Instant.now();
+ AccessToken accessToken = new AccessToken(this.tokenType, this.tokenValue, issuedAt,
+ issuedAt.plusSeconds(this.expiresIn), this.scopes, this.additionalParameters);
+
+ TokenResponseAttributes tokenResponse = new TokenResponseAttributes();
+ tokenResponse.accessToken = accessToken;
+ return tokenResponse;
+ }
+ }
+}
diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/package-info.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/package-info.java
new file mode 100644
index 0000000000..b529499a2f
--- /dev/null
+++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Support classes that model the request/response messages from the
+ * Authorization Endpoint
+ * and Token Endpoint .
+ */
+package org.springframework.security.oauth2.core.endpoint;
diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/package-info.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/package-info.java
new file mode 100644
index 0000000000..01fd764fbe
--- /dev/null
+++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Core classes and interfaces providing support for the OAuth 2.0 Authorization Framework .
+ */
+package org.springframework.security.oauth2.core;
diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/DefaultOAuth2User.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/DefaultOAuth2User.java
new file mode 100644
index 0000000000..9139dd9b27
--- /dev/null
+++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/DefaultOAuth2User.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.core.user;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.SpringSecurityCoreVersion;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+
+import java.time.Instant;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * The default implementation of an {@link OAuth2User}.
+ *
+ *
+ * User attribute names are not standardized between providers
+ * and therefore it is required that the user supply the key
+ * for the user's "name" attribute to one of the constructors.
+ * The key will be used for accessing the "name" of the
+ * Principal (user) via {@link #getAttributes()}
+ * and returning it from {@link #getName()}.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see OAuth2User
+ */
+public class DefaultOAuth2User implements OAuth2User {
+ private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
+ private final Set authorities;
+ private final Map attributes;
+ private final String nameAttributeKey;
+
+ public DefaultOAuth2User(Map attributes, String nameAttributeKey) {
+ this(Collections.emptySet(), attributes, nameAttributeKey);
+ }
+
+ public DefaultOAuth2User(Set authorities, Map attributes, String nameAttributeKey) {
+ Assert.notNull(authorities, "authorities cannot be null");
+ Assert.notEmpty(attributes, "attributes cannot be empty");
+ Assert.hasText(nameAttributeKey, "nameAttributeKey cannot be empty");
+ if (!attributes.containsKey(nameAttributeKey)) {
+ throw new IllegalArgumentException("Invalid nameAttributeKey: " + nameAttributeKey);
+ }
+ this.authorities = Collections.unmodifiableSet(this.sortAuthorities(authorities));
+ this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(attributes));
+ this.nameAttributeKey = nameAttributeKey;
+ }
+
+ @Override
+ public String getName() {
+ return this.getAttributes().get(this.nameAttributeKey).toString();
+ }
+
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ return this.authorities;
+ }
+
+ @Override
+ public Map getAttributes() {
+ return this.attributes;
+ }
+
+ protected String getAttributeAsString(String key) {
+ Object value = this.getAttributes().get(key);
+ return (value != null ? value.toString() : null);
+ }
+
+ protected Boolean getAttributeAsBoolean(String key) {
+ String value = this.getAttributeAsString(key);
+ return (value != null ? Boolean.valueOf(value) : null);
+ }
+
+ protected Instant getAttributeAsInstant(String key) {
+ String value = this.getAttributeAsString(key);
+ if (value == null) {
+ return null;
+ }
+ try {
+ return Instant.ofEpochSecond(Long.valueOf(value));
+ } catch (NumberFormatException ex) {
+ throw new IllegalArgumentException("Invalid long value: " + ex.getMessage(), ex);
+ }
+ }
+
+ private Set sortAuthorities(Set authorities) {
+ if (CollectionUtils.isEmpty(authorities)) {
+ return Collections.emptySet();
+ }
+
+ SortedSet sortedAuthorities =
+ new TreeSet<>((g1, g2) -> g1.getAuthority().compareTo(g2.getAuthority()));
+ authorities.stream().forEach(sortedAuthorities::add);
+
+ return sortedAuthorities;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || this.getClass() != obj.getClass()) {
+ return false;
+ }
+
+ DefaultOAuth2User that = (DefaultOAuth2User) obj;
+
+ if (!this.getName().equals(that.getName())) {
+ return false;
+ }
+ if (!this.getAuthorities().equals(that.getAuthorities())) {
+ return false;
+ }
+ return this.getAttributes().equals(that.getAttributes());
+ }
+
+ @Override
+ public int hashCode() {
+ int result = this.getName().hashCode();
+ result = 31 * result + this.getAuthorities().hashCode();
+ result = 31 * result + this.getAttributes().hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Name: [");
+ sb.append(this.getName());
+ sb.append("], Granted Authorities: [");
+ sb.append(this.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining(", ")));
+ sb.append("], User Attributes: [");
+ sb.append(this.getAttributes().entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining(", ")));
+ sb.append("]");
+ return sb.toString();
+ }
+}
diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/OAuth2User.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/OAuth2User.java
new file mode 100644
index 0000000000..769cd3ba8b
--- /dev/null
+++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/OAuth2User.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.core.user;
+
+import org.springframework.security.core.AuthenticatedPrincipal;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * A representation of a user Principal
+ * that is registered with a standard OAuth 2.0 Provider .
+ *
+ *
+ * An OAuth 2.0 user is composed of one or more attributes, for example,
+ * first name, middle name, last name, email, phone number, address, etc.
+ * Each user attribute has a "name" and "value" and
+ * is keyed by the "name" in {@link #getAttributes()}.
+ *
+ *
+ * NOTE: Attribute names are not standardized between providers and therefore will vary.
+ * Please consult the provider's API documentation for the set of supported user attribute names.
+ *
+ *
+ * Implementation instances of this interface represent an {@link AuthenticatedPrincipal}
+ * which is associated to an {@link Authentication} object
+ * and may be accessed via {@link Authentication#getPrincipal()}.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see DefaultOAuth2User
+ * @see AuthenticatedPrincipal
+ */
+public interface OAuth2User extends AuthenticatedPrincipal, Serializable {
+
+ Collection extends GrantedAuthority> getAuthorities();
+
+ Map getAttributes();
+}
diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/package-info.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/package-info.java
new file mode 100644
index 0000000000..5223cbf768
--- /dev/null
+++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Provides a model for an OAuth 2.0 representation of a user Principal.
+ */
+package org.springframework.security.oauth2.core.user;
diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/StandardClaimName.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/StandardClaimName.java
new file mode 100644
index 0000000000..858174e54b
--- /dev/null
+++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/StandardClaimName.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.oidc;
+
+/**
+ * The Standard Claims defined by the OpenID Connect Core 1.0 specification
+ * and returned in either the UserInfo Response or in the ID Token .
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see Standard Claims
+ * @see UserInfo Response
+ * @see ID Token
+ */
+public interface StandardClaimName {
+
+ String SUB = "sub";
+
+ String NAME = "name";
+
+ String GIVEN_NAME = "given_name";
+
+ String FAMILY_NAME = "family_name";
+
+ String MIDDLE_NAME = "middle_name";
+
+ String NICKNAME = "nickname";
+
+ String PREFERRED_USERNAME = "preferred_username";
+
+ String PROFILE = "profile";
+
+ String PICTURE = "picture";
+
+ String WEBSITE = "website";
+
+ String EMAIL = "email";
+
+ String EMAIL_VERIFIED = "email_verified";
+
+ String GENDER = "gender";
+
+ String BIRTHDATE = "birthdate";
+
+ String ZONEINFO = "zoneinfo";
+
+ String LOCALE = "locale";
+
+ String PHONE_NUMBER = "phone_number";
+
+ String PHONE_NUMBER_VERIFIED = "phone_number_verified";
+
+ String ADDRESS = "address";
+
+ String UPDATED_AT = "updated_at";
+}
diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/package-info.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/package-info.java
new file mode 100644
index 0000000000..3aad8e1a64
--- /dev/null
+++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Core classes and interfaces providing support for OpenID Connect Core 1.0 .
+ */
+package org.springframework.security.oauth2.oidc;
diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/DefaultUserInfo.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/DefaultUserInfo.java
new file mode 100644
index 0000000000..8d2f1f6ac7
--- /dev/null
+++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/DefaultUserInfo.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.oidc.user;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
+import org.springframework.security.oauth2.oidc.StandardClaimName;
+
+import java.time.Instant;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+import static org.springframework.security.oauth2.oidc.StandardClaimName.*;
+
+/**
+ * The default implementation of a {@link UserInfo}.
+ *
+ *
+ * The key used for accessing the "name" of the
+ * Principal (user) via {@link #getAttributes()}
+ * is {@link StandardClaimName#NAME} or if not available
+ * will default to {@link StandardClaimName#SUB}.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see UserInfo
+ * @see DefaultOAuth2User
+ */
+public class DefaultUserInfo extends DefaultOAuth2User implements UserInfo {
+
+ public DefaultUserInfo(Map attributes) {
+ this(Collections.emptySet(), attributes);
+ }
+
+ public DefaultUserInfo(Set authorities, Map attributes) {
+ super(authorities, attributes, SUB);
+ }
+
+ @Override
+ public String getSubject() {
+ return this.getAttributeAsString(SUB);
+ }
+
+ @Override
+ public String getName() {
+ String name = this.getAttributeAsString(NAME);
+ return (name != null ? name : super.getName());
+ }
+
+ @Override
+ public String getGivenName() {
+ return this.getAttributeAsString(GIVEN_NAME);
+ }
+
+ @Override
+ public String getFamilyName() {
+ return this.getAttributeAsString(FAMILY_NAME);
+ }
+
+ @Override
+ public String getMiddleName() {
+ return this.getAttributeAsString(MIDDLE_NAME);
+ }
+
+ @Override
+ public String getNickName() {
+ return this.getAttributeAsString(NICKNAME);
+ }
+
+ @Override
+ public String getPreferredUsername() {
+ return this.getAttributeAsString(PREFERRED_USERNAME);
+ }
+
+ @Override
+ public String getProfile() {
+ return this.getAttributeAsString(PROFILE);
+ }
+
+ @Override
+ public String getPicture() {
+ return this.getAttributeAsString(PICTURE);
+ }
+
+ @Override
+ public String getWebsite() {
+ return this.getAttributeAsString(WEBSITE);
+ }
+
+ @Override
+ public String getEmail() {
+ return this.getAttributeAsString(EMAIL);
+ }
+
+ @Override
+ public Boolean getEmailVerified() {
+ return this.getAttributeAsBoolean(EMAIL_VERIFIED);
+ }
+
+ @Override
+ public String getGender() {
+ return this.getAttributeAsString(GENDER);
+ }
+
+ @Override
+ public String getBirthdate() {
+ return this.getAttributeAsString(BIRTHDATE);
+ }
+
+ @Override
+ public String getZoneInfo() {
+ return this.getAttributeAsString(ZONEINFO);
+ }
+
+ @Override
+ public String getLocale() {
+ return this.getAttributeAsString(LOCALE);
+ }
+
+ @Override
+ public String getPhoneNumber() {
+ return this.getAttributeAsString(PHONE_NUMBER);
+ }
+
+ @Override
+ public Boolean getPhoneNumberVerified() {
+ return this.getAttributeAsBoolean(PHONE_NUMBER_VERIFIED);
+ }
+
+ @Override
+ public Address getAddress() {
+ // TODO Impl
+ return null;
+ }
+
+ @Override
+ public Instant getUpdatedAt() {
+ return this.getAttributeAsInstant(UPDATED_AT);
+ }
+}
diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/UserInfo.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/UserInfo.java
new file mode 100644
index 0000000000..cd5861c012
--- /dev/null
+++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/UserInfo.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.oidc.user;
+
+import org.springframework.security.core.AuthenticatedPrincipal;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+
+import java.time.Instant;
+
+/**
+ * A representation of a user Principal
+ * that is registered with an OpenID Connect 1.0 Provider .
+ *
+ *
+ * The structure of the user Principal is defined by the
+ * UserInfo Endpoint ,
+ * which is an OAuth 2.0 Protected Resource that returns a set of
+ * Claims
+ * about the authenticated End-User.
+ *
+ *
+ * Implementation instances of this interface represent an {@link AuthenticatedPrincipal}
+ * which is associated to an {@link Authentication} object
+ * and may be accessed via {@link Authentication#getPrincipal()}.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see DefaultUserInfo
+ * @see AuthenticatedPrincipal
+ * @see OpenID Connect Core 1.0
+ * @see UserInfo Endpoint
+ * @see Standard Claims
+ */
+public interface UserInfo extends OAuth2User {
+
+ String getSubject();
+
+ String getGivenName();
+
+ String getFamilyName();
+
+ String getMiddleName();
+
+ String getNickName();
+
+ String getPreferredUsername();
+
+ String getProfile();
+
+ String getPicture();
+
+ String getWebsite();
+
+ String getEmail();
+
+ Boolean getEmailVerified();
+
+ String getGender();
+
+ String getBirthdate();
+
+ String getZoneInfo();
+
+ String getLocale();
+
+ String getPhoneNumber();
+
+ Boolean getPhoneNumberVerified();
+
+ Address getAddress();
+
+ Instant getUpdatedAt();
+
+
+ interface Address {
+
+ String getFormatted();
+
+ String getStreetAddress();
+
+ String getLocality();
+
+ String getRegion();
+
+ String getPostalCode();
+
+ String getCountry();
+ }
+}
diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/package-info.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/package-info.java
new file mode 100644
index 0000000000..357fb0e8fa
--- /dev/null
+++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Provides a model for an OpenID Connect Core 1.0 representation of a user Principal.
+ */
+package org.springframework.security.oauth2.oidc.user;
diff --git a/samples/boot/helloworld/pom.xml b/samples/boot/helloworld/pom.xml
index 2575ea08b2..c86924e55b 100644
--- a/samples/boot/helloworld/pom.xml
+++ b/samples/boot/helloworld/pom.xml
@@ -44,6 +44,13 @@
pom
import
+
+ org.springframework.boot
+ spring-boot-dependencies
+ 1.5.0.BUILD-SNAPSHOT
+ pom
+ import
+
diff --git a/samples/boot/insecure/pom.xml b/samples/boot/insecure/pom.xml
index 8d3149705a..08a85db947 100644
--- a/samples/boot/insecure/pom.xml
+++ b/samples/boot/insecure/pom.xml
@@ -44,6 +44,13 @@
pom
import
+
+ org.springframework.boot
+ spring-boot-dependencies
+ 1.5.0.BUILD-SNAPSHOT
+ pom
+ import
+
diff --git a/samples/boot/oauth2login/README.adoc b/samples/boot/oauth2login/README.adoc
new file mode 100644
index 0000000000..d1e1224bf2
--- /dev/null
+++ b/samples/boot/oauth2login/README.adoc
@@ -0,0 +1,342 @@
+= OAuth 2.0 Login Sample
+Joe Grandja
+:toc:
+:security-site-url: https://projects.spring.io/spring-security/
+
+[.lead]
+This guide will walk you through the steps for setting up the sample application with OAuth 2.0 Login using an external _OAuth 2.0_ or _OpenID Connect 1.0_ Provider.
+The sample application is built with *Spring Boot 1.5* and the *spring-security-oauth2-client* module that is new in {security-site-url}[Spring Security 5.0].
+
+The following sections outline detailed steps for setting up OAuth 2.0 Login with these Providers:
+
+* <>
+* <>
+* <>
+* <>
+
+NOTE: The _"authentication flow"_ is realized using the *Authorization Code Grant*, as specified in the https://tools.ietf.org/html/rfc6749#section-4.1[OAuth 2.0 Authorization Framework].
+
+[[sample-app-content]]
+== Sample application content
+
+The sample application contains the following package structure and artifacts:
+
+*org.springframework.security.samples*
+
+[circle]
+* _OAuth2LoginApplication_ - the main class for the _Spring application_.
+** *user*
+*** _GitHubOAuth2User_ - a custom _UserInfo_ type for <>.
+** *web*
+*** _MainController_ - the root controller that displays user information after a successful login.
+
+*org.springframework.boot.autoconfigure.security.oauth2.client*
+
+[circle]
+* <> - a Spring Boot auto-configuration class
+ that automatically registers a _ClientRegistrationRepository_ bean in the _ApplicationContext_.
+* <> - a Spring Boot auto-configuration class that automatically enables OAuth 2.0 Login.
+
+WARNING: The Spring Boot auto-configuration classes (and dependent resources) will eventually _live_ in the *Spring Boot Security Starter*.
+
+NOTE: See <> for a detailed overview of the auto-configuration classes.
+
+[[google-login]]
+== Setting up *_Login with Google_*
+
+The goal for this section of the guide is to setup login using Google as the _Authentication Provider_.
+
+NOTE: https://developers.google.com/identity/protocols/OpenIDConnect[Google's OAuth 2.0 implementation] for authentication conforms to the
+ http://openid.net/connect/[OpenID Connect] specification and is http://openid.net/certification/[OpenID Certified].
+
+[[google-login-register-credentials]]
+=== Register OAuth 2.0 credentials
+
+In order to use Google's OAuth 2.0 authentication system for login, you must set up a project in the *Google API Console* to obtain OAuth 2.0 credentials.
+
+Follow the instructions on the https://developers.google.com/identity/protocols/OpenIDConnect[OpenID Connect] page starting in the section *_"Setting up OAuth 2.0"_*.
+
+After completing the sub-section, *_"Obtain OAuth 2.0 credentials"_*, you should have created a new *OAuth Client* with credentials consisting of a *Client ID* and *Client Secret*.
+
+[[google-login-redirect-uri]]
+=== Setting the redirect URI
+
+The redirect URI is the path in the sample application that the end-user's user-agent is redirected back to after they have authenticated with Google
+and have granted access to the OAuth Client _(created from the <>)_ on the *Consent screen* page.
+
+For the sub-section, *_"Set a redirect URI"_*, ensure the *Authorised redirect URIs* is set to *http://localhost:8080/oauth2/authorize/code/google*
+
+TIP: The default redirect URI is *_"{scheme}://{serverName}:{serverPort}/oauth2/authorize/code/{clientAlias}"_*.
+ See <> for more details on this default.
+
+[[google-login-configure-application-yml]]
+=== Configuring application.yml
+
+Now that we have created a new OAuth Client with Google, we need to configure the sample application to use this OAuth Client for the _authentication flow_.
+
+Go to *_src/main/resources_* and edit *application.yml*. Add the following configuration:
+
+[source,yaml]
+----
+security:
+ oauth2:
+ client:
+ google:
+ client-id: ${client-id}
+ client-secret: ${client-secret}
+----
+
+Replace *${client-id}* and *${client-secret}* with the OAuth 2.0 credentials created in the previous section <>.
+
+[TIP]
+.OAuth client properties
+====
+. *security.oauth2.client* is the *_base property prefix_* for OAuth client properties.
+. Just below the *_base property prefix_* is the *_client property key_*, for example *security.oauth2.client.google*.
+. At the base of the *_client property key_* are the properties for specifying the configuration for an OAuth Client.
+ A list of these properties are detailed in <>.
+====
+
+[[google-login-run-sample]]
+=== Running the sample
+
+Launch the Spring Boot application by running *_org.springframework.security.samples.OAuth2LoginApplication_*.
+
+After the application successfully starts up, go to http://localhost:8080. You will be redirected to http://localhost:8080/login, which will display an _auto-generated login page_ with an anchor link for *Google*.
+
+Click through on the Google link and you'll be redirected to Google for authentication.
+
+After you authenticate using your Google credentials, the next page presented to you will be the *Consent screen*.
+The Consent screen will ask you to either *_Allow_* or *_Deny_* access to the OAuth Client you created in the previous step <>.
+Click *_Allow_* to authorize the OAuth Client to access your _email address_ and _basic profile_ information.
+
+At this point, the OAuth Client will retrieve your email address and basic profile information from the http://openid.net/specs/openid-connect-core-1_0.html#UserInfo[*UserInfo Endpoint*] and establish an _authenticated session_.
+The home page will then be displayed showing the user attributes retrieved from the *UserInfo Endpoint*, for example, name, email, profile, sub, etc.
+
+[[oauth2-login-auto-configuration]]
+== OAuth 2.0 Login auto-configuration
+
+As you worked through this guide and setup OAuth 2.0 Login with one of the Providers,
+we hope you noticed the ease in configuration and setup required in getting the sample up and running?
+And you may be asking, how does this all work? Thanks to some Spring Boot auto-configuration _magic_,
+we were able to automatically register the OAuth Client(s) configured in the `Environment`,
+as well, provide a minimal security configuration for OAuth 2.0 Login for these registered OAuth Client(s).
+
+The following provides an overview of the Spring Boot auto-configuration classes:
+
+[[client-registration-auto-configuration-class]]
+*_org.springframework.boot.autoconfigure.security.oauth2.client.ClientRegistrationAutoConfiguration_*::
+`ClientRegistrationAutoConfiguration` is responsible for registering a `ClientRegistrationRepository` _bean_ with the `ApplicationContext`.
+The `ClientRegistrationRepository` is composed of one or more `ClientRegistration` instances, which are created from the OAuth client properties
+configured in the `Environment` that are prefixed with `security.oauth2.client.[client-alias]`, for example, `security.oauth2.client.google`.
+
+NOTE: `ClientRegistrationAutoConfiguration` also loads a _resource_ named *oauth2-clients-defaults.yml*,
+ which provides a set of default client property values for a number of _well-known_ Providers.
+ More on this in the later section <>.
+
+[[oauth2-login-auto-configuration-class]]
+*_org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2LoginAutoConfiguration_*::
+`OAuth2LoginAutoConfiguration` is responsible for enabling OAuth 2.0 Login,
+only if there is a `ClientRegistrationRepository` _bean_ available in the `ApplicationContext`.
+
+WARNING: The auto-configuration classes (and dependent resources) will eventually _live_ in the *Spring Boot Security Starter*.
+
+[[oauth2-client-properties]]
+=== OAuth client properties
+
+The following specifies the common set of properties available for configuring an OAuth Client.
+
+[TIP]
+====
+- *security.oauth2.client* is the *_base property prefix_* for OAuth client properties.
+- Just below the *_base property prefix_* is the *_client property key_*, for example *security.oauth2.client.google*.
+- At the base of the *_client property key_* are the properties for specifying the configuration for an OAuth Client.
+====
+
+- *client-authentication-method* - the method used to authenticate the _Client_ with the _Provider_. Supported values are *header* and *form*.
+- *authorized-grant-type* - the OAuth 2.0 Authorization Framework defines the https://tools.ietf.org/html/rfc6749#section-1.3.1[Authorization Code] grant type,
+ which is used to realize the _"authentication flow"_. Currently, this is the only supported grant type.
+- *redirect-uri* - this is the client's _registered_ redirect URI that the _Authorization Server_ redirects the end-user's user-agent
+ to after the end-user has authenticated and authorized access for the client.
+
+NOTE: The default redirect URI is _"{scheme}://{serverName}:{serverPort}/oauth2/authorize/code/{clientAlias}"_, which leverages *URI template variables*.
+
+- *scopes* - a comma-delimited string of scope(s) requested during the _Authorization Request_ flow, for example: _openid, email, profile_
+
+NOTE: _OpenID Connect 1.0_ defines these http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims[standard scopes]: _profile, email, address, phone_
+
+NOTE: Non-standard scopes may be defined by a standard _OAuth 2.0 Provider_. Please consult the Provider's OAuth API documentation to learn which scopes are supported.
+
+- *authorization-uri* - the URI used by the client to redirect the end-user's user-agent to the _Authorization Server_ in order to obtain authorization from the end-user (the _Resource Owner_).
+- *token-uri* - the URI used by the client when exchanging an _Authorization Grant_ (for example, Authorization Code) for an _Access Token_ at the _Authorization Server_.
+- *user-info-uri* - the URI used by the client to access the protected resource *UserInfo Endpoint*, in order to obtain attributes of the end-user.
+- *user-info-converter* - the `Converter` implementation class used to convert the *UserInfo Response* to a `UserInfo` (_OpenID Connect 1.0 Provider_) or `OAuth2User` instance (_Standard OAuth 2.0 Provider_).
+
+TIP: The `Converter` implementation class for an _OpenID Connect 1.0 Provider_ is *org.springframework.security.oauth2.client.user.converter.UserInfoConverter*
+ and for a standard _OAuth 2.0 Provider_ it's *org.springframework.security.oauth2.client.user.converter.OAuth2UserConverter*.
+
+- *user-info-name-attribute-key* - the _key_ used to retrieve the *Name* of the end-user from the `Map` of available attributes in `UserInfo` or `OAuth2User`.
+
+NOTE: _OpenID Connect 1.0_ defines the http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims[*"name"* Claim], which is the end-user's full name and is the default used for `UserInfo`.
+
+IMPORTANT: Standard _OAuth 2.0 Provider's_ may vary the naming of their *Name* attribute. Please consult the Provider's *UserInfo* API documentation.
+ This is a *_required_* property when *user-info-converter* is set to `OAuth2UserConverter`.
+
+- *client-name* - this is a descriptive name used for the client. The name may be used in certain scenarios, for example, when displaying the name of the client in the _auto-generated login page_.
+- *client-alias* - an _alias_ which uniquely identifies the client. It *must be* unique within a `ClientRegistrationRepository`.
+
+[[oauth2-default-client-properties]]
+=== Default client property values
+
+As noted previously, <> loads a _resource_ named *oauth2-clients-defaults.yml*,
+which provides a set of default client property values for a number of _well-known_ Providers.
+
+For example, the *authorization-uri*, *token-uri*, *user-info-uri* rarely change for a Provider and therefore it makes sense to
+provide a set of defaults in order to reduce the configuration required by the user.
+
+Below are the current set of default client property values:
+
+.oauth2-clients-defaults.yml
+[source,yaml]
+----
+security:
+ oauth2:
+ client:
+ google:
+ client-authentication-method: header
+ authorized-grant-type: authorization_code
+ redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
+ scopes: openid, email, profile
+ authorization-uri: "https://accounts.google.com/o/oauth2/auth"
+ token-uri: "https://accounts.google.com/o/oauth2/token"
+ user-info-uri: "https://www.googleapis.com/oauth2/v3/userinfo"
+ user-info-converter: "org.springframework.security.oauth2.client.user.converter.UserInfoConverter"
+ client-name: Google
+ client-alias: google
+ github:
+ client-authentication-method: header
+ authorized-grant-type: authorization_code
+ redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
+ scopes: user
+ authorization-uri: "https://github.com/login/oauth/authorize"
+ token-uri: "https://github.com/login/oauth/access_token"
+ user-info-uri: "https://api.github.com/user"
+ user-info-converter: "org.springframework.security.oauth2.client.user.converter.OAuth2UserConverter"
+ client-name: GitHub
+ client-alias: github
+ facebook:
+ client-authentication-method: form
+ authorized-grant-type: authorization_code
+ redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
+ scopes: public_profile, email
+ authorization-uri: "https://www.facebook.com/v2.8/dialog/oauth"
+ token-uri: "https://graph.facebook.com/v2.8/oauth/access_token"
+ user-info-uri: "https://graph.facebook.com/me"
+ user-info-converter: "org.springframework.security.oauth2.client.user.converter.OAuth2UserConverter"
+ client-name: Facebook
+ client-alias: facebook
+ okta:
+ client-authentication-method: header
+ authorized-grant-type: authorization_code
+ redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
+ scopes: openid, email, profile
+ user-info-converter: "org.springframework.security.oauth2.client.user.converter.UserInfoConverter"
+ client-name: Okta
+ client-alias: okta
+----
+
+= Appendix
+'''
+
+[[configure-non-spring-boot-app]]
+== Configuring a _Non-Spring-Boot_ application
+
+If you are not using Spring Boot for your application, you will not be able to leverage the auto-configuration features for OAuth 2.0 Login.
+You will be required to provide your own _security configuration_ in order to enable OAuth 2.0 Login.
+
+The following sample code demonstrates a minimal security configuration for enabling OAuth 2.0 Login.
+
+Assuming we have a _properties file_ named *oauth2-clients.properties* on the _classpath_ and it specifies all the _required_ properties for an OAuth Client, specifically _"Google"_:
+
+.oauth2-clients.properties
+[source,properties]
+----
+security.oauth2.client.google.client-id=${client-id}
+security.oauth2.client.google.client-secret=${client-secret}
+security.oauth2.client.google.client-authentication-method=header
+security.oauth2.client.google.authorized-grant-type=authorization_code
+security.oauth2.client.google.redirect-uri=http://localhost:8080/oauth2/authorize/code/google
+security.oauth2.client.google.scopes=openid,email,profile
+security.oauth2.client.google.authorization-uri=https://accounts.google.com/o/oauth2/auth
+security.oauth2.client.google.token-uri=https://accounts.google.com/o/oauth2/token
+security.oauth2.client.google.user-info-uri=https://www.googleapis.com/oauth2/v3/userinfo
+security.oauth2.client.google.user-info-converter=org.springframework.security.oauth2.client.user.converter.UserInfoConverter
+security.oauth2.client.google.client-name=Google
+security.oauth2.client.google.client-alias=google
+----
+
+The following _security configuration_ will enable OAuth 2.0 Login using _"Google"_ as the _Authentication Provider_:
+
+[source,java]
+----
+@EnableWebSecurity
+@PropertySource("classpath:oauth2-clients.properties")
+public class SecurityConfig extends WebSecurityConfigurerAdapter {
+ private Environment environment;
+
+ public SecurityConfig(Environment environment) {
+ this.environment = environment;
+ }
+
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+ http
+ .authorizeRequests()
+ .anyRequest().authenticated()
+ .and()
+ .oauth2Login()
+ .clients(clientRegistrationRepository())
+ .userInfoEndpoint()
+ .userInfoTypeConverter(
+ new UserInfoConverter(),
+ new URI("https://www.googleapis.com/oauth2/v3/userinfo"));
+ }
+
+ @Bean
+ public ClientRegistrationRepository clientRegistrationRepository() {
+ List clientRegistrations = Collections.singletonList(
+ clientRegistration("security.oauth2.client.google."));
+
+ return new InMemoryClientRegistrationRepository(clientRegistrations);
+ }
+
+ private ClientRegistration clientRegistration(String clientPropertyKey) {
+ String clientId = this.environment.getProperty(clientPropertyKey + "client-id");
+ String clientSecret = this.environment.getProperty(clientPropertyKey + "client-secret");
+ ClientAuthenticationMethod clientAuthenticationMethod = ClientAuthenticationMethod.valueOf(
+ this.environment.getProperty(clientPropertyKey + "client-authentication-method").toUpperCase());
+ AuthorizationGrantType authorizationGrantType = AuthorizationGrantType.valueOf(
+ this.environment.getProperty(clientPropertyKey + "authorized-grant-type").toUpperCase());
+ String redirectUri = this.environment.getProperty(clientPropertyKey + "redirect-uri");
+ String[] scopes = this.environment.getProperty(clientPropertyKey + "scopes").split(",");
+ String authorizationUri = this.environment.getProperty(clientPropertyKey + "authorization-uri");
+ String tokenUri = this.environment.getProperty(clientPropertyKey + "token-uri");
+ String userInfoUri = this.environment.getProperty(clientPropertyKey + "user-info-uri");
+ String clientName = this.environment.getProperty(clientPropertyKey + "client-name");
+ String clientAlias = this.environment.getProperty(clientPropertyKey + "client-alias");
+
+ return new ClientRegistration.Builder(clientId)
+ .clientSecret(clientSecret)
+ .clientAuthenticationMethod(clientAuthenticationMethod)
+ .authorizedGrantType(authorizationGrantType)
+ .redirectUri(redirectUri)
+ .scopes(scopes)
+ .authorizationUri(authorizationUri)
+ .tokenUri(tokenUri)
+ .userInfoUri(userInfoUri)
+ .clientName(clientName)
+ .clientAlias(clientAlias)
+ .build();
+ }
+}
+----
diff --git a/samples/boot/oauth2login/pom.xml b/samples/boot/oauth2login/pom.xml
new file mode 100644
index 0000000000..537f636f77
--- /dev/null
+++ b/samples/boot/oauth2login/pom.xml
@@ -0,0 +1,173 @@
+
+
+ 4.0.0
+ org.springframework.security
+ spring-security-samples-boot-oauth2login
+ 5.0.0.BUILD-SNAPSHOT
+ spring-security-samples-boot-oauth2login
+ spring-security-samples-boot-oauth2login
+ http://spring.io/spring-security
+
+ spring.io
+ http://spring.io/
+
+
+
+ The Apache Software License, Version 2.0
+ http://www.apache.org/licenses/LICENSE-2.0.txt
+ repo
+
+
+
+
+ rwinch
+ Rob Winch
+ rwinch@pivotal.io
+
+
+ jgrandja
+ Joe Grandja
+ jgrandja@pivotal.io
+
+
+
+ scm:git:git://github.com/spring-projects/spring-security
+ scm:git:git://github.com/spring-projects/spring-security
+ https://github.com/spring-projects/spring-security
+
+
+
+
+ org.springframework
+ spring-framework-bom
+ 4.3.5.RELEASE
+ pom
+ import
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ 1.5.0.BUILD-SNAPSHOT
+ pom
+ import
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+ compile
+
+
+ org.springframework.boot
+ spring-boot-starter-thymeleaf
+ compile
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+ compile
+
+
+ org.springframework.security
+ spring-security-config
+ 5.0.0.BUILD-SNAPSHOT
+ compile
+
+
+ org.springframework.security
+ spring-security-oauth2-client
+ 5.0.0.BUILD-SNAPSHOT
+ compile
+
+
+ org.springframework.security
+ spring-security-web
+ 5.0.0.BUILD-SNAPSHOT
+ compile
+
+
+ org.thymeleaf.extras
+ thymeleaf-extras-springsecurity4
+ 2.1.3.RELEASE
+ compile
+
+
+ commons-logging
+ commons-logging
+ 1.2
+ compile
+ true
+
+
+ ch.qos.logback
+ logback-classic
+ 1.1.2
+ test
+
+
+ junit
+ junit
+ 4.12
+ test
+
+
+ net.sourceforge.htmlunit
+ htmlunit
+ 2.24
+ test
+
+
+ org.assertj
+ assertj-core
+ 3.6.2
+ test
+
+
+ org.mockito
+ mockito-core
+ 1.10.19
+ test
+
+
+ org.slf4j
+ jcl-over-slf4j
+ 1.7.7
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.security
+ spring-security-test
+ 5.0.0.BUILD-SNAPSHOT
+ test
+
+
+ org.springframework
+ spring-test
+ test
+
+
+
+
+ spring-snapshot
+ https://repo.spring.io/snapshot
+
+
+
+
+
+ maven-compiler-plugin
+
+ 1.8
+ 1.8
+
+
+
+
+
diff --git a/samples/boot/oauth2login/spring-security-samples-boot-oauth2login.gradle b/samples/boot/oauth2login/spring-security-samples-boot-oauth2login.gradle
new file mode 100644
index 0000000000..08e35c3a74
--- /dev/null
+++ b/samples/boot/oauth2login/spring-security-samples-boot-oauth2login.gradle
@@ -0,0 +1,15 @@
+apply plugin: 'io.spring.convention.spring-sample-boot'
+
+dependencies {
+ compile project(':spring-security-config')
+ compile project(':spring-security-oauth2-client')
+ compile project(':spring-security-web')
+ compile 'org.springframework.boot:spring-boot-starter-security'
+ compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
+ compile 'org.springframework.boot:spring-boot-starter-web'
+ compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity4'
+
+ testCompile project(':spring-security-test')
+ testCompile 'net.sourceforge.htmlunit:htmlunit'
+ testCompile 'org.springframework.boot:spring-boot-starter-test'
+}
diff --git a/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java b/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java
new file mode 100644
index 0000000000..07df61415f
--- /dev/null
+++ b/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java
@@ -0,0 +1,405 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.samples;
+
+import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
+import com.gargoylesoftware.htmlunit.WebClient;
+import com.gargoylesoftware.htmlunit.WebResponse;
+import com.gargoylesoftware.htmlunit.html.DomNodeList;
+import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
+import com.gargoylesoftware.htmlunit.html.HtmlElement;
+import com.gargoylesoftware.htmlunit.html.HtmlPage;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationProcessingFilter;
+import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationToken;
+import org.springframework.security.oauth2.client.authentication.AuthorizationCodeRequestRedirectFilter;
+import org.springframework.security.oauth2.client.authentication.AuthorizationGrantTokenExchanger;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import org.springframework.security.oauth2.client.user.OAuth2UserService;
+import org.springframework.security.oauth2.core.AccessToken;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.endpoint.OAuth2Parameter;
+import org.springframework.security.oauth2.core.endpoint.ResponseType;
+import org.springframework.security.oauth2.core.endpoint.TokenResponseAttributes;
+import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.web.util.UriComponents;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.net.URI;
+import java.net.URL;
+import java.net.URLDecoder;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Integration tests for the OAuth 2.0 client filters {@link AuthorizationCodeRequestRedirectFilter}
+ * and {@link AuthorizationCodeAuthenticationProcessingFilter}.
+ * These filters work together to realize the Authorization Code Grant flow.
+ *
+ * @author Joe Grandja
+ */
+@RunWith(SpringRunner.class)
+@SpringBootTest
+@AutoConfigureMockMvc
+public class OAuth2LoginApplicationTests {
+ private static final String AUTHORIZATION_BASE_URI = "/oauth2/authorization/code";
+ private static final String AUTHORIZE_BASE_URL = "http://localhost:8080/oauth2/authorize/code";
+
+ @Autowired
+ private WebClient webClient;
+
+ @Autowired
+ private ClientRegistrationRepository clientRegistrationRepository;
+
+ private ClientRegistration googleClientRegistration;
+ private ClientRegistration githubClientRegistration;
+ private ClientRegistration facebookClientRegistration;
+ private ClientRegistration oktaClientRegistration;
+
+ @Before
+ public void setup() {
+ this.webClient.getCookieManager().clearCookies();
+ this.googleClientRegistration = this.clientRegistrationRepository.getRegistrationByClientAlias("google");
+ this.githubClientRegistration = this.clientRegistrationRepository.getRegistrationByClientAlias("github");
+ this.facebookClientRegistration = this.clientRegistrationRepository.getRegistrationByClientAlias("facebook");
+ this.oktaClientRegistration = this.clientRegistrationRepository.getRegistrationByClientAlias("okta");
+ }
+
+ @Test
+ public void requestRootPageWhenNotAuthenticatedThenDisplayLoginPage() throws Exception {
+ HtmlPage page = this.webClient.getPage("/");
+ this.assertLoginPage(page);
+ }
+
+ @Test
+ public void requestOtherPageWhenNotAuthenticatedThenDisplayLoginPage() throws Exception {
+ HtmlPage page = this.webClient.getPage("/other-page");
+ this.assertLoginPage(page);
+ }
+
+ @Test
+ public void requestAuthorizeGitHubClientWhenLinkClickedThenStatusRedirectForAuthorization() throws Exception {
+ HtmlPage page = this.webClient.getPage("/");
+
+ HtmlAnchor clientAnchorElement = this.getClientAnchorElement(page, this.githubClientRegistration);
+ assertThat(clientAnchorElement).isNotNull();
+
+ WebResponse response = this.followLinkDisableRedirects(clientAnchorElement);
+
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.MOVED_PERMANENTLY.value());
+
+ String authorizeRedirectUri = response.getResponseHeaderValue("Location");
+ assertThat(authorizeRedirectUri).isNotNull();
+
+ UriComponents uriComponents = UriComponentsBuilder.fromUri(URI.create(authorizeRedirectUri)).build();
+
+ String requestUri = uriComponents.getScheme() + "://" + uriComponents.getHost() + uriComponents.getPath();
+ assertThat(requestUri).isEqualTo(this.githubClientRegistration.getProviderDetails().getAuthorizationUri().toString());
+
+ Map params = uriComponents.getQueryParams().toSingleValueMap();
+
+ assertThat(params.get(OAuth2Parameter.RESPONSE_TYPE)).isEqualTo(ResponseType.CODE.value());
+ assertThat(params.get(OAuth2Parameter.CLIENT_ID)).isEqualTo(this.githubClientRegistration.getClientId());
+ String redirectUri = AUTHORIZE_BASE_URL + "/" + this.githubClientRegistration.getClientAlias();
+ assertThat(URLDecoder.decode(params.get(OAuth2Parameter.REDIRECT_URI), "UTF-8")).isEqualTo(redirectUri);
+ assertThat(URLDecoder.decode(params.get(OAuth2Parameter.SCOPE), "UTF-8"))
+ .isEqualTo(this.githubClientRegistration.getScopes().stream().collect(Collectors.joining(" ")));
+ assertThat(params.get(OAuth2Parameter.STATE)).isNotNull();
+ }
+
+ @Test
+ public void requestAuthorizeClientWhenInvalidClientThenStatusBadRequest() throws Exception {
+ HtmlPage page = this.webClient.getPage("/");
+
+ HtmlAnchor clientAnchorElement = this.getClientAnchorElement(page, this.googleClientRegistration);
+ assertThat(clientAnchorElement).isNotNull();
+ clientAnchorElement.setAttribute("href", clientAnchorElement.getHrefAttribute() + "-invalid");
+
+ WebResponse response = null;
+ try {
+ clientAnchorElement.click();
+ } catch (FailingHttpStatusCodeException ex) {
+ response = ex.getResponse();
+ }
+
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value());
+ }
+
+ @Test
+ public void requestAuthorizationCodeGrantWhenValidAuthorizationResponseThenDisplayUserInfoPage() throws Exception {
+ HtmlPage page = this.webClient.getPage("/");
+
+ HtmlAnchor clientAnchorElement = this.getClientAnchorElement(page, this.githubClientRegistration);
+ assertThat(clientAnchorElement).isNotNull();
+
+ WebResponse response = this.followLinkDisableRedirects(clientAnchorElement);
+
+ UriComponents authorizeRequestUriComponents = UriComponentsBuilder.fromUri(
+ URI.create(response.getResponseHeaderValue("Location"))).build();
+
+ Map params = authorizeRequestUriComponents.getQueryParams().toSingleValueMap();
+ String code = "auth-code";
+ String state = URLDecoder.decode(params.get(OAuth2Parameter.STATE), "UTF-8");
+ String redirectUri = URLDecoder.decode(params.get(OAuth2Parameter.REDIRECT_URI), "UTF-8");
+
+ String authorizationResponseUri =
+ UriComponentsBuilder.fromHttpUrl(redirectUri)
+ .queryParam(OAuth2Parameter.CODE, code)
+ .queryParam(OAuth2Parameter.STATE, state)
+ .build().encode().toUriString();
+
+ page = this.webClient.getPage(new URL(authorizationResponseUri));
+ this.assertUserInfoPage(page);
+ }
+
+ @Test
+ public void requestAuthorizationCodeGrantWhenNoMatchingAuthorizationRequestThenDisplayLoginPageWithError() throws Exception {
+ HtmlPage page = this.webClient.getPage("/");
+ URL loginPageUrl = page.getBaseURL();
+ URL loginErrorPageUrl = new URL(loginPageUrl.toString() + "?error");
+
+ String code = "auth-code";
+ String state = "state";
+ String redirectUri = AUTHORIZE_BASE_URL + "/" + this.googleClientRegistration.getClientAlias();
+
+ String authorizationResponseUri =
+ UriComponentsBuilder.fromHttpUrl(redirectUri)
+ .queryParam(OAuth2Parameter.CODE, code)
+ .queryParam(OAuth2Parameter.STATE, state)
+ .build().encode().toUriString();
+
+ // Clear session cookie will ensure the 'session-saved'
+ // Authorization Request (from previous request) is not found
+ this.webClient.getCookieManager().clearCookies();
+
+ page = this.webClient.getPage(new URL(authorizationResponseUri));
+ assertThat(page.getBaseURL()).isEqualTo(loginErrorPageUrl);
+
+ HtmlElement errorElement = page.getBody().getFirstByXPath("p");
+ assertThat(errorElement).isNotNull();
+ assertThat(errorElement.asText()).contains("authorization_request_not_found");
+ }
+
+ @Test
+ public void requestAuthorizationCodeGrantWhenInvalidStateParamThenDisplayLoginPageWithError() throws Exception {
+ HtmlPage page = this.webClient.getPage("/");
+ URL loginPageUrl = page.getBaseURL();
+ URL loginErrorPageUrl = new URL(loginPageUrl.toString() + "?error");
+
+ HtmlAnchor clientAnchorElement = this.getClientAnchorElement(page, this.googleClientRegistration);
+ assertThat(clientAnchorElement).isNotNull();
+ this.followLinkDisableRedirects(clientAnchorElement);
+
+ String code = "auth-code";
+ String state = "invalid-state";
+ String redirectUri = AUTHORIZE_BASE_URL + "/" + this.githubClientRegistration.getClientAlias();
+
+ String authorizationResponseUri =
+ UriComponentsBuilder.fromHttpUrl(redirectUri)
+ .queryParam(OAuth2Parameter.CODE, code)
+ .queryParam(OAuth2Parameter.STATE, state)
+ .build().encode().toUriString();
+
+ page = this.webClient.getPage(new URL(authorizationResponseUri));
+ assertThat(page.getBaseURL()).isEqualTo(loginErrorPageUrl);
+
+ HtmlElement errorElement = page.getBody().getFirstByXPath("p");
+ assertThat(errorElement).isNotNull();
+ assertThat(errorElement.asText()).contains("invalid_state_parameter");
+ }
+
+ @Test
+ public void requestAuthorizationCodeGrantWhenInvalidRedirectUriThenDisplayLoginPageWithError() throws Exception {
+ HtmlPage page = this.webClient.getPage("/");
+ URL loginPageUrl = page.getBaseURL();
+ URL loginErrorPageUrl = new URL(loginPageUrl.toString() + "?error");
+
+ HtmlAnchor clientAnchorElement = this.getClientAnchorElement(page, this.googleClientRegistration);
+ assertThat(clientAnchorElement).isNotNull();
+
+ WebResponse response = this.followLinkDisableRedirects(clientAnchorElement);
+
+ UriComponents authorizeRequestUriComponents = UriComponentsBuilder.fromUri(
+ URI.create(response.getResponseHeaderValue("Location"))).build();
+
+ Map params = authorizeRequestUriComponents.getQueryParams().toSingleValueMap();
+ String code = "auth-code";
+ String state = URLDecoder.decode(params.get(OAuth2Parameter.STATE), "UTF-8");
+ String redirectUri = URLDecoder.decode(params.get(OAuth2Parameter.REDIRECT_URI), "UTF-8");
+ redirectUri += "-invalid";
+
+ String authorizationResponseUri =
+ UriComponentsBuilder.fromHttpUrl(redirectUri)
+ .queryParam(OAuth2Parameter.CODE, code)
+ .queryParam(OAuth2Parameter.STATE, state)
+ .build().encode().toUriString();
+
+ page = this.webClient.getPage(new URL(authorizationResponseUri));
+ assertThat(page.getBaseURL()).isEqualTo(loginErrorPageUrl);
+
+ HtmlElement errorElement = page.getBody().getFirstByXPath("p");
+ assertThat(errorElement).isNotNull();
+ assertThat(errorElement.asText()).contains("invalid_redirect_uri_parameter");
+ }
+
+ @Test
+ public void requestAuthorizationCodeGrantWhenStandardErrorCodeResponseThenDisplayLoginPageWithError() throws Exception {
+ HtmlPage page = this.webClient.getPage("/");
+ URL loginPageUrl = page.getBaseURL();
+ URL loginErrorPageUrl = new URL(loginPageUrl.toString() + "?error");
+
+ String error = OAuth2Error.INVALID_CLIENT_ERROR_CODE;
+ String state = "state";
+ String redirectUri = AUTHORIZE_BASE_URL + "/" + this.githubClientRegistration.getClientAlias();
+
+ String authorizationResponseUri =
+ UriComponentsBuilder.fromHttpUrl(redirectUri)
+ .queryParam(OAuth2Parameter.ERROR, error)
+ .queryParam(OAuth2Parameter.STATE, state)
+ .build().encode().toUriString();
+
+ page = this.webClient.getPage(new URL(authorizationResponseUri));
+ assertThat(page.getBaseURL()).isEqualTo(loginErrorPageUrl);
+
+ HtmlElement errorElement = page.getBody().getFirstByXPath("p");
+ assertThat(errorElement).isNotNull();
+ assertThat(errorElement.asText()).contains(error);
+ }
+
+ private void assertLoginPage(HtmlPage page) throws Exception {
+ assertThat(page.getTitleText()).isEqualTo("Login Page");
+
+ int expectedClients = 4;
+
+ List clientAnchorElements = page.getAnchors();
+ assertThat(clientAnchorElements.size()).isEqualTo(expectedClients);
+
+ String baseAuthorizeUri = AUTHORIZATION_BASE_URI + "/";
+ String googleClientAuthorizeUri = baseAuthorizeUri + this.googleClientRegistration.getClientAlias();
+ String githubClientAuthorizeUri = baseAuthorizeUri + this.githubClientRegistration.getClientAlias();
+ String facebookClientAuthorizeUri = baseAuthorizeUri + this.facebookClientRegistration.getClientAlias();
+ String oktaClientAuthorizeUri = baseAuthorizeUri + this.oktaClientRegistration.getClientAlias();
+
+ for (int i=0; i divElements = page.getBody().getElementsByTagName("div");
+ assertThat(divElements.get(1).asText()).contains("User: joeg@springsecurity.io");
+ assertThat(divElements.get(4).asText()).contains("Name: joeg@springsecurity.io");
+ }
+
+ private HtmlAnchor getClientAnchorElement(HtmlPage page, ClientRegistration clientRegistration) {
+ Optional clientAnchorElement = page.getAnchors().stream()
+ .filter(e -> e.asText().equals(clientRegistration.getClientName())).findFirst();
+
+ return (clientAnchorElement.isPresent() ? clientAnchorElement.get() : null);
+ }
+
+ private WebResponse followLinkDisableRedirects(HtmlAnchor anchorElement) throws Exception {
+ WebResponse response = null;
+ try {
+ // Disable the automatic redirection (which will trigger
+ // an exception) so that we can capture the response
+ this.webClient.getOptions().setRedirectEnabled(false);
+ anchorElement.click();
+ } catch (FailingHttpStatusCodeException ex) {
+ response = ex.getResponse();
+ this.webClient.getOptions().setRedirectEnabled(true);
+ }
+ return response;
+ }
+
+ @EnableWebSecurity
+ public static class SecurityTestConfig extends WebSecurityConfigurerAdapter {
+
+ // @formatter:off
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+ http
+ .authorizeRequests()
+ .anyRequest().authenticated()
+ .and()
+ .oauth2Login()
+ .authorizationCodeTokenExchanger(this.mockAuthorizationCodeTokenExchanger())
+ .userInfoEndpoint()
+ .userInfoService(this.mockUserInfoService());
+ }
+ // @formatter:on
+
+ private AuthorizationGrantTokenExchanger mockAuthorizationCodeTokenExchanger() {
+ TokenResponseAttributes tokenResponse = TokenResponseAttributes.withToken("access-token-1234")
+ .tokenType(AccessToken.TokenType.BEARER)
+ .expiresIn(60 * 1000)
+ .scopes(Collections.singleton("openid"))
+ .build();
+
+ AuthorizationGrantTokenExchanger mock = mock(AuthorizationGrantTokenExchanger.class);
+ when(mock.exchange(any())).thenReturn(tokenResponse);
+ return mock;
+ }
+
+ private OAuth2UserService mockUserInfoService() {
+ Map attributes = new HashMap<>();
+ attributes.put("id", "joeg");
+ attributes.put("first-name", "Joe");
+ attributes.put("last-name", "Grandja");
+ attributes.put("email", "joeg@springsecurity.io");
+
+ DefaultOAuth2User user = new DefaultOAuth2User(attributes, "email");
+
+ OAuth2UserService mock = mock(OAuth2UserService.class);
+ when(mock.loadUser(any())).thenReturn(user);
+ return mock;
+ }
+ }
+
+ @SpringBootConfiguration
+ @EnableAutoConfiguration
+ @ComponentScan(basePackages = "org.springframework.security.samples.web")
+ public static class SpringBootApplicationTestConfig {
+ }
+}
diff --git a/samples/boot/oauth2login/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ClientRegistrationAutoConfiguration.java b/samples/boot/oauth2login/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ClientRegistrationAutoConfiguration.java
new file mode 100644
index 0000000000..5f1c952c07
--- /dev/null
+++ b/samples/boot/oauth2login/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ClientRegistrationAutoConfiguration.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.boot.autoconfigure.security.oauth2.client;
+
+import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
+import org.springframework.boot.autoconfigure.AutoConfigureBefore;
+import org.springframework.boot.autoconfigure.condition.*;
+import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration;
+import org.springframework.boot.bind.PropertySourcesBinder;
+import org.springframework.boot.bind.RelaxedPropertyResolver;
+import org.springframework.context.annotation.*;
+import org.springframework.core.env.ConfigurableEnvironment;
+import org.springframework.core.env.Environment;
+import org.springframework.core.env.MutablePropertySources;
+import org.springframework.core.env.PropertiesPropertySource;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.type.AnnotatedTypeMetadata;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationProperties;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
+import org.springframework.util.CollectionUtils;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @author Joe Grandja
+ */
+@Configuration
+@ConditionalOnWebApplication
+@ConditionalOnClass(ClientRegistrationRepository.class)
+@ConditionalOnMissingBean(ClientRegistrationRepository.class)
+@AutoConfigureBefore(SecurityAutoConfiguration.class)
+public class ClientRegistrationAutoConfiguration {
+ private static final String CLIENT_ID_PROPERTY = "client-id";
+ private static final String CLIENTS_DEFAULTS_RESOURCE = "META-INF/oauth2-clients-defaults.yml";
+ static final String CLIENT_PROPERTY_PREFIX = "security.oauth2.client.";
+
+ @Configuration
+ @Conditional(ClientPropertiesAvailableCondition.class)
+ protected static class ClientRegistrationConfiguration {
+ private final Environment environment;
+
+ protected ClientRegistrationConfiguration(Environment environment) {
+ this.environment = environment;
+ }
+
+ @Bean
+ public ClientRegistrationRepository clientRegistrationRepository() {
+ MutablePropertySources propertySources = ((ConfigurableEnvironment) this.environment).getPropertySources();
+ Properties clientsDefaultProperties = this.getClientsDefaultProperties();
+ if (clientsDefaultProperties != null) {
+ propertySources.addLast(new PropertiesPropertySource("oauth2ClientsDefaults", clientsDefaultProperties));
+ }
+ PropertySourcesBinder binder = new PropertySourcesBinder(propertySources);
+ RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(this.environment, CLIENT_PROPERTY_PREFIX);
+
+ List clientRegistrations = new ArrayList<>();
+
+ Set clientPropertyKeys = resolveClientPropertyKeys(this.environment);
+ for (String clientPropertyKey : clientPropertyKeys) {
+ if (!resolver.containsProperty(clientPropertyKey + "." + CLIENT_ID_PROPERTY)) {
+ continue;
+ }
+ ClientRegistrationProperties clientRegistrationProperties = new ClientRegistrationProperties();
+ binder.bindTo(CLIENT_PROPERTY_PREFIX + clientPropertyKey, clientRegistrationProperties);
+ ClientRegistration clientRegistration = new ClientRegistration.Builder(clientRegistrationProperties).build();
+ clientRegistrations.add(clientRegistration);
+ }
+
+ return new InMemoryClientRegistrationRepository(clientRegistrations);
+ }
+
+ private Properties getClientsDefaultProperties() {
+ ClassPathResource clientsDefaultsResource = new ClassPathResource(CLIENTS_DEFAULTS_RESOURCE);
+ if (!clientsDefaultsResource.exists()) {
+ return null;
+ }
+ YamlPropertiesFactoryBean yamlPropertiesFactory = new YamlPropertiesFactoryBean();
+ yamlPropertiesFactory.setResources(clientsDefaultsResource);
+ return yamlPropertiesFactory.getObject();
+ }
+ }
+
+ static Set resolveClientPropertyKeys(Environment environment) {
+ Set clientPropertyKeys = new LinkedHashSet<>();
+ RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(environment, CLIENT_PROPERTY_PREFIX);
+ resolver.getSubProperties("").keySet().forEach(key -> {
+ int endIndex = key.indexOf('.');
+ if (endIndex != -1) {
+ clientPropertyKeys.add(key.substring(0, endIndex));
+ }
+ });
+ return clientPropertyKeys;
+ }
+
+ private static class ClientPropertiesAvailableCondition extends SpringBootCondition implements ConfigurationCondition {
+
+ @Override
+ public ConfigurationCondition.ConfigurationPhase getConfigurationPhase() {
+ return ConfigurationPhase.PARSE_CONFIGURATION;
+ }
+
+ @Override
+ public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
+ ConditionMessage.Builder message = ConditionMessage.forCondition("OAuth2 Client Properties");
+ Set clientPropertyKeys = resolveClientPropertyKeys(context.getEnvironment());
+ if (!CollectionUtils.isEmpty(clientPropertyKeys)) {
+ return ConditionOutcome.match(message.foundExactly("OAuth2 Client(s) -> " +
+ clientPropertyKeys.stream().collect(Collectors.joining(", "))));
+ }
+ return ConditionOutcome.noMatch(message.notAvailable("OAuth2 Client(s)"));
+ }
+ }
+}
diff --git a/samples/boot/oauth2login/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2LoginAutoConfiguration.java b/samples/boot/oauth2login/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2LoginAutoConfiguration.java
new file mode 100644
index 0000000000..0e80a457f4
--- /dev/null
+++ b/samples/boot/oauth2login/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2LoginAutoConfiguration.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.boot.autoconfigure.security.oauth2.client;
+
+import org.springframework.boot.autoconfigure.AutoConfigureAfter;
+import org.springframework.boot.autoconfigure.AutoConfigureBefore;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
+import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.core.env.Environment;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import org.springframework.security.oauth2.client.user.converter.AbstractOAuth2UserConverter;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.util.ClassUtils;
+
+import java.lang.reflect.Constructor;
+import java.net.URI;
+import java.util.Set;
+
+import static org.springframework.boot.autoconfigure.security.oauth2.client.ClientRegistrationAutoConfiguration.CLIENT_PROPERTY_PREFIX;
+import static org.springframework.boot.autoconfigure.security.oauth2.client.ClientRegistrationAutoConfiguration.resolveClientPropertyKeys;
+
+/**
+ * @author Joe Grandja
+ */
+@Configuration
+@ConditionalOnWebApplication
+@ConditionalOnClass(EnableWebSecurity.class)
+@ConditionalOnMissingBean(WebSecurityConfiguration.class)
+@ConditionalOnBean(ClientRegistrationRepository.class)
+@AutoConfigureBefore(SecurityAutoConfiguration.class)
+@AutoConfigureAfter(ClientRegistrationAutoConfiguration.class)
+public class OAuth2LoginAutoConfiguration {
+ private static final String USER_INFO_URI_PROPERTY = "user-info-uri";
+ private static final String USER_INFO_CONVERTER_PROPERTY = "user-info-converter";
+ private static final String USER_INFO_NAME_ATTR_KEY_PROPERTY = "user-info-name-attribute-key";
+
+ @EnableWebSecurity
+ protected static class OAuth2LoginSecurityConfiguration extends WebSecurityConfigurerAdapter {
+ private final Environment environment;
+
+ protected OAuth2LoginSecurityConfiguration(Environment environment) {
+ this.environment = environment;
+ }
+
+ // @formatter:off
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+ http
+ .authorizeRequests()
+ .antMatchers("/favicon.ico").permitAll()
+ .anyRequest().authenticated()
+ .and()
+ .oauth2Login();
+
+ this.registerUserInfoTypeConverters(http.oauth2Login());
+ }
+ // @formatter:on
+
+ private void registerUserInfoTypeConverters(OAuth2LoginConfigurer oauth2LoginConfigurer) throws Exception {
+ Set clientPropertyKeys = resolveClientPropertyKeys(this.environment);
+ for (String clientPropertyKey : clientPropertyKeys) {
+ String fullClientPropertyKey = CLIENT_PROPERTY_PREFIX + clientPropertyKey + ".";
+ String userInfoUriValue = this.environment.getProperty(fullClientPropertyKey + USER_INFO_URI_PROPERTY);
+ String userInfoConverterTypeValue = this.environment.getProperty(fullClientPropertyKey + USER_INFO_CONVERTER_PROPERTY);
+ if (userInfoUriValue != null && userInfoConverterTypeValue != null) {
+ Class extends Converter> userInfoConverterType = ClassUtils.resolveClassName(
+ userInfoConverterTypeValue, this.getClass().getClassLoader()).asSubclass(Converter.class);
+ Converter 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)oauth2UserConverterConstructor.newInstance(userInfoNameAttributeKey);
+ }
+ }
+ if (userInfoConverter == null) {
+ userInfoConverter = (Converter)userInfoConverterType.newInstance();
+ }
+ oauth2LoginConfigurer.userInfoEndpoint().userInfoTypeConverter(userInfoConverter, new URI(userInfoUriValue));
+ }
+ }
+ }
+ }
+}
diff --git a/samples/boot/oauth2login/src/main/java/org/springframework/security/samples/OAuth2LoginApplication.java b/samples/boot/oauth2login/src/main/java/org/springframework/security/samples/OAuth2LoginApplication.java
new file mode 100644
index 0000000000..3f70a8b17e
--- /dev/null
+++ b/samples/boot/oauth2login/src/main/java/org/springframework/security/samples/OAuth2LoginApplication.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.samples;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * @author Joe Grandja
+ */
+@SpringBootApplication
+public class OAuth2LoginApplication {
+
+ public OAuth2LoginApplication() {
+ }
+
+ public static void main(String[] args) {
+ SpringApplication.run(OAuth2LoginApplication.class, args);
+ }
+
+}
diff --git a/samples/boot/oauth2login/src/main/java/org/springframework/security/samples/user/GitHubOAuth2User.java b/samples/boot/oauth2login/src/main/java/org/springframework/security/samples/user/GitHubOAuth2User.java
new file mode 100644
index 0000000000..10254cc579
--- /dev/null
+++ b/samples/boot/oauth2login/src/main/java/org/springframework/security/samples/user/GitHubOAuth2User.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.samples.user;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author Joe Grandja
+ */
+public class GitHubOAuth2User implements OAuth2User {
+ private String id;
+ private String name;
+ private String login;
+ private String email;
+
+ public GitHubOAuth2User() {
+ }
+
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public Map getAttributes() {
+ Map attributes = new HashMap<>();
+ attributes.put("id", this.getId());
+ attributes.put("name", this.getName());
+ attributes.put("login", this.getLogin());
+ attributes.put("email", this.getEmail());
+ return attributes;
+ }
+
+ public String getId() {
+ return this.id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ @Override
+ public String getName() {
+ return this.name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getLogin() {
+ return this.login;
+ }
+
+ public void setLogin(String login) {
+ this.login = login;
+ }
+
+ public String getEmail() {
+ return this.email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+}
diff --git a/samples/boot/oauth2login/src/main/java/org/springframework/security/samples/web/MainController.java b/samples/boot/oauth2login/src/main/java/org/springframework/security/samples/web/MainController.java
new file mode 100644
index 0000000000..8fdb843022
--- /dev/null
+++ b/samples/boot/oauth2login/src/main/java/org/springframework/security/samples/web/MainController.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.samples.web;
+
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+/**
+ * @author Joe Grandja
+ */
+@Controller
+public class MainController {
+
+ @RequestMapping("/")
+ public String index(Model model, @AuthenticationPrincipal OAuth2User user) {
+ model.addAttribute("userName", user.getName());
+ model.addAttribute("userAttributes", user.getAttributes());
+ return "index";
+ }
+}
diff --git a/samples/boot/oauth2login/src/main/resources/META-INF/oauth2-clients-defaults.yml b/samples/boot/oauth2login/src/main/resources/META-INF/oauth2-clients-defaults.yml
new file mode 100644
index 0000000000..6ad5ed450a
--- /dev/null
+++ b/samples/boot/oauth2login/src/main/resources/META-INF/oauth2-clients-defaults.yml
@@ -0,0 +1,44 @@
+security:
+ oauth2:
+ client:
+ google:
+ client-authentication-method: header
+ authorized-grant-type: authorization_code
+ redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
+ scopes: openid, email, profile
+ authorization-uri: "https://accounts.google.com/o/oauth2/auth"
+ token-uri: "https://accounts.google.com/o/oauth2/token"
+ user-info-uri: "https://www.googleapis.com/oauth2/v3/userinfo"
+ user-info-converter: "org.springframework.security.oauth2.client.user.converter.UserInfoConverter"
+ client-name: Google
+ client-alias: google
+ github:
+ client-authentication-method: header
+ authorized-grant-type: authorization_code
+ redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
+ scopes: user
+ authorization-uri: "https://github.com/login/oauth/authorize"
+ token-uri: "https://github.com/login/oauth/access_token"
+ user-info-uri: "https://api.github.com/user"
+ user-info-converter: "org.springframework.security.oauth2.client.user.converter.OAuth2UserConverter"
+ client-name: GitHub
+ client-alias: github
+ facebook:
+ client-authentication-method: form
+ authorized-grant-type: authorization_code
+ redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
+ scopes: public_profile, email
+ authorization-uri: "https://www.facebook.com/v2.8/dialog/oauth"
+ token-uri: "https://graph.facebook.com/v2.8/oauth/access_token"
+ user-info-uri: "https://graph.facebook.com/me"
+ user-info-converter: "org.springframework.security.oauth2.client.user.converter.OAuth2UserConverter"
+ client-name: Facebook
+ client-alias: facebook
+ okta:
+ client-authentication-method: header
+ authorized-grant-type: authorization_code
+ redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
+ scopes: openid, email, profile
+ user-info-converter: "org.springframework.security.oauth2.client.user.converter.UserInfoConverter"
+ client-name: Okta
+ client-alias: okta
diff --git a/samples/boot/oauth2login/src/main/resources/META-INF/spring.factories b/samples/boot/oauth2login/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000000..de3be686f9
--- /dev/null
+++ b/samples/boot/oauth2login/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,4 @@
+# Spring Boot Auto Configurations
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+org.springframework.boot.autoconfigure.security.oauth2.client.ClientRegistrationAutoConfiguration,\
+org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2LoginAutoConfiguration
diff --git a/samples/boot/oauth2login/src/main/resources/application.yml b/samples/boot/oauth2login/src/main/resources/application.yml
new file mode 100644
index 0000000000..faa25818f7
--- /dev/null
+++ b/samples/boot/oauth2login/src/main/resources/application.yml
@@ -0,0 +1,34 @@
+server:
+ port: 8080
+
+logging:
+ level:
+ root: INFO
+ org.springframework.web: INFO
+ org.springframework.security: INFO
+# org.springframework.boot.autoconfigure: DEBUG
+
+spring:
+ thymeleaf:
+ cache: false
+
+security:
+ oauth2:
+ client:
+ google:
+ client-id: your-app-client-id
+ client-secret: your-app-client-secret
+ github:
+ client-id: your-app-client-id
+ client-secret: your-app-client-secret
+ user-info-name-attribute-key: "name"
+ facebook:
+ client-id: your-app-client-id
+ client-secret: your-app-client-secret
+ user-info-name-attribute-key: "name"
+ okta:
+ client-id: your-app-client-id
+ client-secret: your-app-client-secret
+ authorization-uri: https://your-subdomain.oktapreview.com/oauth2/v1/authorize
+ token-uri: https://your-subdomain.oktapreview.com/oauth2/v1/token
+ user-info-uri: https://your-subdomain.oktapreview.com/oauth2/v1/userinfo
diff --git a/samples/boot/oauth2login/src/main/resources/templates/index.html b/samples/boot/oauth2login/src/main/resources/templates/index.html
new file mode 100644
index 0000000000..cd7c31f9d1
--- /dev/null
+++ b/samples/boot/oauth2login/src/main/resources/templates/index.html
@@ -0,0 +1,33 @@
+
+
+
+ Spring Security - OAuth2 User Info
+
+
+
+
+OAuth2 User Info
+
+ Name:
+
+
+
+
+
diff --git a/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java b/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java
index 8561b6d559..00b0488172 100644
--- a/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java
+++ b/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java
@@ -15,7 +15,13 @@
*/
package org.springframework.security.web.authentication.ui;
-import java.io.IOException;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.WebAttributes;
+import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices;
+import org.springframework.security.web.csrf.CsrfToken;
+import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
@@ -24,14 +30,8 @@ import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
-
-import org.springframework.security.core.AuthenticationException;
-import org.springframework.security.web.WebAttributes;
-import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
-import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
-import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices;
-import org.springframework.security.web.csrf.CsrfToken;
-import org.springframework.web.filter.GenericFilterBean;
+import java.io.IOException;
+import java.util.Map;
/**
* For internal use with namespace configuration in the case where a user doesn't
@@ -51,6 +51,7 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
private String failureUrl;
private boolean formLoginEnabled;
private boolean openIdEnabled;
+ private boolean oauth2LoginEnabled;
private String authenticationUrl;
private String usernameParameter;
private String passwordParameter;
@@ -58,6 +59,8 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
private String openIDauthenticationUrl;
private String openIDusernameParameter;
private String openIDrememberMeParameter;
+ private Map oauth2AuthenticationUrlToClientName;
+
public DefaultLoginPageGeneratingFilter() {
}
@@ -105,7 +108,7 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
}
public boolean isEnabled() {
- return formLoginEnabled || openIdEnabled;
+ return formLoginEnabled || openIdEnabled || oauth2LoginEnabled;
}
public void setLogoutSuccessUrl(String logoutSuccessUrl) {
@@ -132,6 +135,10 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
this.openIdEnabled = openIdEnabled;
}
+ public void setOauth2LoginEnabled(boolean oauth2LoginEnabled) {
+ this.oauth2LoginEnabled = oauth2LoginEnabled;
+ }
+
public void setAuthenticationUrl(String authenticationUrl) {
this.authenticationUrl = authenticationUrl;
}
@@ -157,6 +164,10 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
this.openIDusernameParameter = openIDusernameParameter;
}
+ public void setOauth2AuthenticationUrlToClientName(Map oauth2AuthenticationUrlToClientName) {
+ this.oauth2AuthenticationUrlToClientName = oauth2AuthenticationUrlToClientName;
+ }
+
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
@@ -201,13 +212,13 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
}
if (loginError) {
- sb.append("Your login attempt was not successful, try again. Reason: ");
+ sb.append("Your login attempt was not successful, try again. Reason: ");
sb.append(errorMsg);
- sb.append("
");
+ sb.append("
");
}
if (logoutSuccess) {
- sb.append("You have been logged out
");
+ sb.append("You have been logged out
");
}
if (formLoginEnabled) {
@@ -252,6 +263,19 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
sb.append("");
}
+ if (oauth2LoginEnabled) {
+ sb.append("Login with OAuth 2.0 ");
+ sb.append("\n");
+ }
+
sb.append("");
return sb.toString();