parent
a38352c4cc
commit
829c386756
|
@ -113,6 +113,13 @@
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-oauth2-client</artifactId>
|
||||||
|
<version>5.0.0.BUILD-SNAPSHOT</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.security</groupId>
|
<groupId>org.springframework.security</groupId>
|
||||||
<artifactId>spring-security-openid</artifactId>
|
<artifactId>spring-security-openid</artifactId>
|
||||||
|
|
|
@ -12,6 +12,7 @@ dependencies {
|
||||||
|
|
||||||
optional project(':spring-security-ldap')
|
optional project(':spring-security-ldap')
|
||||||
optional project(':spring-security-messaging')
|
optional project(':spring-security-messaging')
|
||||||
|
optional project(':spring-security-oauth2-client')
|
||||||
optional project(':spring-security-openid')
|
optional project(':spring-security-openid')
|
||||||
optional project(':spring-security-web')
|
optional project(':spring-security-web')
|
||||||
optional 'org.aspectj:aspectjweaver'
|
optional 'org.aspectj:aspectjweaver'
|
||||||
|
|
|
@ -77,6 +77,10 @@ final class FilterComparator implements Comparator<Filter>, Serializable {
|
||||||
order += STEP;
|
order += STEP;
|
||||||
put(LogoutFilter.class, order);
|
put(LogoutFilter.class, order);
|
||||||
order += STEP;
|
order += STEP;
|
||||||
|
filterToOrder.put(
|
||||||
|
"org.springframework.security.oauth2.client.authentication.AuthorizationCodeRequestRedirectFilter",
|
||||||
|
order);
|
||||||
|
order += STEP;
|
||||||
put(X509AuthenticationFilter.class, order);
|
put(X509AuthenticationFilter.class, order);
|
||||||
order += STEP;
|
order += STEP;
|
||||||
put(AbstractPreAuthenticatedProcessingFilter.class, order);
|
put(AbstractPreAuthenticatedProcessingFilter.class, order);
|
||||||
|
@ -84,6 +88,10 @@ final class FilterComparator implements Comparator<Filter>, Serializable {
|
||||||
filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter",
|
filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter",
|
||||||
order);
|
order);
|
||||||
order += STEP;
|
order += STEP;
|
||||||
|
filterToOrder.put(
|
||||||
|
"org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationProcessingFilter",
|
||||||
|
order);
|
||||||
|
order += STEP;
|
||||||
put(UsernamePasswordAuthenticationFilter.class, order);
|
put(UsernamePasswordAuthenticationFilter.class, order);
|
||||||
order += STEP;
|
order += STEP;
|
||||||
put(ConcurrentSessionFilter.class, order);
|
put(ConcurrentSessionFilter.class, order);
|
||||||
|
|
|
@ -61,6 +61,7 @@ import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.context.SecurityContext;
|
import org.springframework.security.core.context.SecurityContext;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
|
||||||
import org.springframework.security.web.DefaultSecurityFilterChain;
|
import org.springframework.security.web.DefaultSecurityFilterChain;
|
||||||
import org.springframework.security.web.PortMapper;
|
import org.springframework.security.web.PortMapper;
|
||||||
import org.springframework.security.web.PortMapperImpl;
|
import org.springframework.security.web.PortMapperImpl;
|
||||||
|
@ -896,6 +897,158 @@ public final class HttpSecurity extends
|
||||||
return getOrApply(new FormLoginConfigurer<HttpSecurity>());
|
return getOrApply(new FormLoginConfigurer<HttpSecurity>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures authentication against an external <i>OAuth 2.0</i> or <i>OpenID Connect 1.0</i> Provider.
|
||||||
|
* <br>
|
||||||
|
* <br>
|
||||||
|
*
|
||||||
|
* The <i>"authentication flow"</i> is realized using the <b>Authorization Code Grant</b>,
|
||||||
|
* as specified in the <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">OAuth 2.0 Authorization Framework</a>.
|
||||||
|
* <br>
|
||||||
|
* <br>
|
||||||
|
*
|
||||||
|
* As a prerequisite to using this feature, the developer must register a <i>Client</i> with an <i>Authorization Server</i>.
|
||||||
|
* The output of the <i>Client Registration</i> process results in a number of properties that are then used for configuring
|
||||||
|
* an instance of a {@link org.springframework.security.oauth2.client.registration.ClientRegistration}.
|
||||||
|
* Properties specific to a <i>Client</i> include: <i>client_id</i>, <i>client_secret</i>, <i>scope</i>, <i>redirect_uri</i>, etc.
|
||||||
|
* There are also properties specific to the <i>Provider</i>, for example,
|
||||||
|
* <i>Authorization Endpoint URI</i>, <i>Token Endpoint URI</i>, <i>UserInfo Endpoint URI</i>, etc.
|
||||||
|
* <br>
|
||||||
|
* <br>
|
||||||
|
*
|
||||||
|
* Multiple client support is provided for use cases where the application provides the user the option
|
||||||
|
* for <i>"Logging in"</i> against one or more Providers, for example, <i>Google</i>, <i>GitHub</i>, <i>Facebook</i>, etc.
|
||||||
|
* <br>
|
||||||
|
* <br>
|
||||||
|
*
|
||||||
|
* {@link org.springframework.security.oauth2.client.registration.ClientRegistration}(s) are composed within a
|
||||||
|
* {@link org.springframework.security.oauth2.client.registration.ClientRegistrationRepository}.
|
||||||
|
* An instance of {@link org.springframework.security.oauth2.client.registration.ClientRegistrationRepository} is <b>required</b>
|
||||||
|
* and may be supplied via the {@link ApplicationContext} or configured using
|
||||||
|
* {@link OAuth2LoginConfigurer#clients(org.springframework.security.oauth2.client.registration.ClientRegistrationRepository)}.
|
||||||
|
* <br>
|
||||||
|
* <br>
|
||||||
|
*
|
||||||
|
* The default configuration provides an auto-generated login page at <code>"/login"</code> and
|
||||||
|
* redirects to <code>"/login?error"</code> when an authentication error occurs.
|
||||||
|
* The login page will display each of the clients (composed within the
|
||||||
|
* {@link org.springframework.security.oauth2.client.registration.ClientRegistrationRepository})
|
||||||
|
* with an anchor link to <code>"/oauth2/authorization/code/{clientAlias}"</code>.
|
||||||
|
* Clicking through the link will initiate the <i>"Authorization Request"</i> flow
|
||||||
|
* redirecting the end-user's user-agent to the <i>Authorization Endpoint</i> of the <i>Provider</i>.
|
||||||
|
* Assuming the <i>Resource Owner</i> (end-user) grants the <i>Client</i> access, the <i>Authorization Server</i>
|
||||||
|
* will redirect the end-user's user-agent to the <i>Redirection Endpoint</i> containing the <i>Authorization Code</i>
|
||||||
|
* - the <i>Redirection Endpoint</i> is automatically configured for the application and
|
||||||
|
* defaults to <code>"/oauth2/authorize/code/{clientAlias}"</code>.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* At this point in the <i>"authentication flow"</i>, the configured
|
||||||
|
* {@link org.springframework.security.oauth2.client.authentication.AuthorizationGrantTokenExchanger}
|
||||||
|
* will exchange the <i>Authorization Code</i> for an <i>Access Token</i> and then use it to access the protected resource
|
||||||
|
* at the <i>UserInfo Endpoint</i> (via {@link org.springframework.security.oauth2.client.user.OAuth2UserService})
|
||||||
|
* in order to retrieve the details of the <i>Resource Owner</i> (end-user) and establish the <i>"authenticated"</i> session.
|
||||||
|
*
|
||||||
|
* <h2>Example Configurations</h2>
|
||||||
|
*
|
||||||
|
* The minimal configuration defaults to automatically generating a login page at <code>"/login"</code>
|
||||||
|
* and redirecting to <code>"/login?error"</code> when an authentication error occurs or redirecting to
|
||||||
|
* <code>"/"</code> when an authenticated session is established.
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* @EnableWebSecurity
|
||||||
|
* public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
|
||||||
|
*
|
||||||
|
* @Override
|
||||||
|
* protected void configure(HttpSecurity http) throws Exception {
|
||||||
|
* http
|
||||||
|
* .authorizeRequests()
|
||||||
|
* .anyRequest().authenticated()
|
||||||
|
* .and()
|
||||||
|
* .oauth2Login();
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @Bean
|
||||||
|
* public ClientRegistrationRepository clientRegistrationRepository() {
|
||||||
|
* // ClientRegistrationRepositoryImpl must be composed of at least one ClientRegistration instance
|
||||||
|
* return new ClientRegistrationRepositoryImpl();
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* The following shows the configuration options available for customizing the defaults.
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* @EnableWebSecurity
|
||||||
|
* public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
|
||||||
|
*
|
||||||
|
* @Override
|
||||||
|
* protected void configure(HttpSecurity http) throws Exception {
|
||||||
|
* http
|
||||||
|
* .authorizeRequests()
|
||||||
|
* .anyRequest().authenticated()
|
||||||
|
* .and()
|
||||||
|
* .oauth2Login()
|
||||||
|
* .clients(this.clientRegistrationRepository())
|
||||||
|
* .authorizationRequestBuilder(this.authorizationRequestBuilder())
|
||||||
|
* .authorizationCodeTokenExchanger(this.authorizationCodeTokenExchanger())
|
||||||
|
* .userInfoEndpoint()
|
||||||
|
* .userInfoService(this.userInfoService())
|
||||||
|
* .userInfoEndpoint()
|
||||||
|
* // Provide a mapping between a Converter implementation and a UserInfo Endpoint URI
|
||||||
|
* .userInfoTypeConverter(this.userInfoConverter(),
|
||||||
|
* new URI("https://www.googleapis.com/oauth2/v3/userinfo"));
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @Bean
|
||||||
|
* public ClientRegistrationRepository clientRegistrationRepository() {
|
||||||
|
* // ClientRegistrationRepositoryImpl must be composed of at least one ClientRegistration instance
|
||||||
|
* return new ClientRegistrationRepositoryImpl();
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @Bean
|
||||||
|
* public AuthorizationRequestUriBuilder authorizationRequestBuilder() {
|
||||||
|
* // Custom URI builder for the "Authorization Request"
|
||||||
|
* return new AuthorizationRequestUriBuilderImpl();
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @Bean
|
||||||
|
* public AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> authorizationCodeTokenExchanger() {
|
||||||
|
* // Custom implementation that exchanges an "Authorization Code Grant" for an "Access Token"
|
||||||
|
* return new AuthorizationCodeTokenExchangerImpl();
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @Bean
|
||||||
|
* public OAuth2UserService userInfoService() {
|
||||||
|
* // Custom implementation that retrieves the details of the authenticated user at the "UserInfo Endpoint"
|
||||||
|
* return new OAuth2UserServiceImpl();
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @Bean
|
||||||
|
* public Converter<ClientHttpResponse, UserInfo> userInfoConverter() {
|
||||||
|
* // Default converter implementation for UserInfo
|
||||||
|
* return new org.springframework.security.oauth2.client.user.converter.UserInfoConverter();
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant Flow</a>
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.1">Section 4.1.1 Authorization Request</a>
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.2">Section 4.1.2 Authorization Response</a>
|
||||||
|
* @see org.springframework.security.oauth2.client.registration.ClientRegistration
|
||||||
|
* @see org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
|
||||||
|
* @see org.springframework.security.oauth2.client.authentication.AuthorizationRequestUriBuilder
|
||||||
|
* @see org.springframework.security.oauth2.client.authentication.AuthorizationGrantTokenExchanger
|
||||||
|
* @see org.springframework.security.oauth2.client.user.OAuth2UserService
|
||||||
|
*
|
||||||
|
* @return the {@link OAuth2LoginConfigurer} for further customizations
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public OAuth2LoginConfigurer<HttpSecurity> oauth2Login() throws Exception {
|
||||||
|
return getOrApply(new OAuth2LoginConfigurer<HttpSecurity>());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures channel security. In order for this configuration to be useful at least
|
* Configures channel security. In order for this configuration to be useful at least
|
||||||
* one mapping to a required channel must be provided.
|
* one mapping to a required channel must be provided.
|
||||||
|
|
|
@ -0,0 +1,123 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.config.annotation.web.configurers.oauth2.client;
|
||||||
|
|
||||||
|
import org.springframework.core.convert.converter.Converter;
|
||||||
|
import org.springframework.http.client.ClientHttpResponse;
|
||||||
|
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationProcessingFilter;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationProvider;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.AuthorizationGrantTokenExchanger;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.nimbus.NimbusAuthorizationCodeTokenExchanger;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||||
|
import org.springframework.security.oauth2.client.user.OAuth2UserService;
|
||||||
|
import org.springframework.security.oauth2.client.user.nimbus.NimbusOAuth2UserService;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
|
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Joe Grandja
|
||||||
|
*/
|
||||||
|
final class AuthorizationCodeAuthenticationFilterConfigurer<H extends HttpSecurityBuilder<H>> extends
|
||||||
|
AbstractAuthenticationFilterConfigurer<H, AuthorizationCodeAuthenticationFilterConfigurer<H>, AuthorizationCodeAuthenticationProcessingFilter> {
|
||||||
|
|
||||||
|
private AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> authorizationCodeTokenExchanger;
|
||||||
|
private OAuth2UserService userInfoService;
|
||||||
|
private Map<URI, Converter<ClientHttpResponse, ? extends OAuth2User>> userInfoTypeConverters = new HashMap<>();
|
||||||
|
|
||||||
|
|
||||||
|
AuthorizationCodeAuthenticationFilterConfigurer() {
|
||||||
|
super(new AuthorizationCodeAuthenticationProcessingFilter(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthorizationCodeAuthenticationFilterConfigurer<H> clientRegistrationRepository(ClientRegistrationRepository clientRegistrationRepository) {
|
||||||
|
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
|
||||||
|
Assert.notEmpty(clientRegistrationRepository.getRegistrations(), "clientRegistrationRepository cannot be empty");
|
||||||
|
this.getBuilder().setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthorizationCodeAuthenticationFilterConfigurer<H> authorizationCodeTokenExchanger(
|
||||||
|
AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> authorizationCodeTokenExchanger) {
|
||||||
|
|
||||||
|
Assert.notNull(authorizationCodeTokenExchanger, "authorizationCodeTokenExchanger cannot be null");
|
||||||
|
this.authorizationCodeTokenExchanger = authorizationCodeTokenExchanger;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthorizationCodeAuthenticationFilterConfigurer<H> userInfoService(OAuth2UserService userInfoService) {
|
||||||
|
Assert.notNull(userInfoService, "userInfoService cannot be null");
|
||||||
|
this.userInfoService = userInfoService;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthorizationCodeAuthenticationFilterConfigurer<H> userInfoTypeConverter(Converter<ClientHttpResponse, ? extends OAuth2User> userInfoConverter, URI userInfoUri) {
|
||||||
|
Assert.notNull(userInfoConverter, "userInfoConverter cannot be null");
|
||||||
|
Assert.notNull(userInfoUri, "userInfoUri cannot be null");
|
||||||
|
this.userInfoTypeConverters.put(userInfoUri, userInfoConverter);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getLoginUrl() {
|
||||||
|
return super.getLoginPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
String getLoginFailureUrl() {
|
||||||
|
return super.getFailureUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(H http) throws Exception {
|
||||||
|
AuthorizationCodeAuthenticationProvider authenticationProvider = new AuthorizationCodeAuthenticationProvider(
|
||||||
|
this.getAuthorizationCodeTokenExchanger(), this.getUserInfoService());
|
||||||
|
authenticationProvider = this.postProcess(authenticationProvider);
|
||||||
|
http.authenticationProvider(authenticationProvider);
|
||||||
|
super.init(http);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(H http) throws Exception {
|
||||||
|
AuthorizationCodeAuthenticationProcessingFilter authFilter = this.getAuthenticationFilter();
|
||||||
|
authFilter.setClientRegistrationRepository(OAuth2LoginConfigurer.getClientRegistrationRepository(this.getBuilder()));
|
||||||
|
super.configure(http);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) {
|
||||||
|
return this.getAuthenticationFilter().getAuthorizeRequestMatcher();
|
||||||
|
}
|
||||||
|
|
||||||
|
private AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> getAuthorizationCodeTokenExchanger() {
|
||||||
|
if (this.authorizationCodeTokenExchanger == null) {
|
||||||
|
this.authorizationCodeTokenExchanger = new NimbusAuthorizationCodeTokenExchanger();
|
||||||
|
}
|
||||||
|
return this.authorizationCodeTokenExchanger;
|
||||||
|
}
|
||||||
|
|
||||||
|
private OAuth2UserService getUserInfoService() {
|
||||||
|
if (this.userInfoService == null) {
|
||||||
|
this.userInfoService = new NimbusOAuth2UserService(this.userInfoTypeConverters);
|
||||||
|
}
|
||||||
|
return this.userInfoService;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.config.annotation.web.configurers.oauth2.client;
|
||||||
|
|
||||||
|
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeRequestRedirectFilter;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.AuthorizationRequestUriBuilder;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.DefaultAuthorizationRequestUriBuilder;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Joe Grandja
|
||||||
|
*/
|
||||||
|
final class AuthorizationCodeRequestRedirectFilterConfigurer<B extends HttpSecurityBuilder<B>> extends
|
||||||
|
AbstractHttpConfigurer<AuthorizationCodeRequestRedirectFilterConfigurer<B>, B> {
|
||||||
|
|
||||||
|
private AuthorizationRequestUriBuilder authorizationRequestBuilder;
|
||||||
|
|
||||||
|
AuthorizationCodeRequestRedirectFilterConfigurer<B> clientRegistrationRepository(ClientRegistrationRepository clientRegistrationRepository) {
|
||||||
|
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
|
||||||
|
Assert.notEmpty(clientRegistrationRepository.getRegistrations(), "clientRegistrationRepository cannot be empty");
|
||||||
|
this.getBuilder().setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthorizationCodeRequestRedirectFilterConfigurer<B> authorizationRequestBuilder(AuthorizationRequestUriBuilder authorizationRequestBuilder) {
|
||||||
|
Assert.notNull(authorizationRequestBuilder, "authorizationRequestBuilder cannot be null");
|
||||||
|
this.authorizationRequestBuilder = authorizationRequestBuilder;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(B http) throws Exception {
|
||||||
|
AuthorizationCodeRequestRedirectFilter filter = new AuthorizationCodeRequestRedirectFilter(
|
||||||
|
OAuth2LoginConfigurer.getClientRegistrationRepository(this.getBuilder()),
|
||||||
|
this.getAuthorizationRequestBuilder());
|
||||||
|
http.addFilter(this.postProcess(filter));
|
||||||
|
}
|
||||||
|
|
||||||
|
private AuthorizationRequestUriBuilder getAuthorizationRequestBuilder() {
|
||||||
|
if (this.authorizationRequestBuilder == null) {
|
||||||
|
this.authorizationRequestBuilder = new DefaultAuthorizationRequestUriBuilder();
|
||||||
|
}
|
||||||
|
return this.authorizationRequestBuilder;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,154 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.config.annotation.web.configurers.oauth2.client;
|
||||||
|
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.core.convert.converter.Converter;
|
||||||
|
import org.springframework.http.client.ClientHttpResponse;
|
||||||
|
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeRequestRedirectFilter;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.AuthorizationGrantTokenExchanger;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.AuthorizationRequestUriBuilder;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||||
|
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
||||||
|
import org.springframework.security.oauth2.client.user.OAuth2UserService;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
|
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Joe Grandja
|
||||||
|
*/
|
||||||
|
public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>> extends
|
||||||
|
AbstractHttpConfigurer<OAuth2LoginConfigurer<B>, B> {
|
||||||
|
|
||||||
|
private final AuthorizationCodeRequestRedirectFilterConfigurer<B> authorizationCodeRequestRedirectFilterConfigurer;
|
||||||
|
private final AuthorizationCodeAuthenticationFilterConfigurer<B> authorizationCodeAuthenticationFilterConfigurer;
|
||||||
|
private final UserInfoEndpointConfig userInfoEndpointConfig;
|
||||||
|
|
||||||
|
public OAuth2LoginConfigurer() {
|
||||||
|
this.authorizationCodeRequestRedirectFilterConfigurer = new AuthorizationCodeRequestRedirectFilterConfigurer<>();
|
||||||
|
this.authorizationCodeAuthenticationFilterConfigurer = new AuthorizationCodeAuthenticationFilterConfigurer<>();
|
||||||
|
this.userInfoEndpointConfig = new UserInfoEndpointConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public OAuth2LoginConfigurer<B> clients(ClientRegistration... clientRegistrations) {
|
||||||
|
Assert.notEmpty(clientRegistrations, "clientRegistrations cannot be empty");
|
||||||
|
return clients(new InMemoryClientRegistrationRepository(Arrays.asList(clientRegistrations)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public OAuth2LoginConfigurer<B> clients(ClientRegistrationRepository clientRegistrationRepository) {
|
||||||
|
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
|
||||||
|
Assert.notEmpty(clientRegistrationRepository.getRegistrations(), "clientRegistrationRepository cannot be empty");
|
||||||
|
this.getBuilder().setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OAuth2LoginConfigurer<B> authorizationRequestBuilder(AuthorizationRequestUriBuilder authorizationRequestBuilder) {
|
||||||
|
Assert.notNull(authorizationRequestBuilder, "authorizationRequestBuilder cannot be null");
|
||||||
|
this.authorizationCodeRequestRedirectFilterConfigurer.authorizationRequestBuilder(authorizationRequestBuilder);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OAuth2LoginConfigurer<B> authorizationCodeTokenExchanger(
|
||||||
|
AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> authorizationCodeTokenExchanger) {
|
||||||
|
|
||||||
|
Assert.notNull(authorizationCodeTokenExchanger, "authorizationCodeTokenExchanger cannot be null");
|
||||||
|
this.authorizationCodeAuthenticationFilterConfigurer.authorizationCodeTokenExchanger(authorizationCodeTokenExchanger);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserInfoEndpointConfig userInfoEndpoint() {
|
||||||
|
return this.userInfoEndpointConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UserInfoEndpointConfig {
|
||||||
|
|
||||||
|
private UserInfoEndpointConfig() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public OAuth2LoginConfigurer<B> userInfoService(OAuth2UserService userInfoService) {
|
||||||
|
Assert.notNull(userInfoService, "userInfoService cannot be null");
|
||||||
|
OAuth2LoginConfigurer.this.authorizationCodeAuthenticationFilterConfigurer.userInfoService(userInfoService);
|
||||||
|
return this.and();
|
||||||
|
}
|
||||||
|
|
||||||
|
public OAuth2LoginConfigurer<B> userInfoTypeConverter(Converter<ClientHttpResponse, ? extends OAuth2User> userInfoConverter, URI userInfoUri) {
|
||||||
|
Assert.notNull(userInfoConverter, "userInfoConverter cannot be null");
|
||||||
|
Assert.notNull(userInfoUri, "userInfoUri cannot be null");
|
||||||
|
OAuth2LoginConfigurer.this.authorizationCodeAuthenticationFilterConfigurer.userInfoTypeConverter(userInfoConverter, userInfoUri);
|
||||||
|
return this.and();
|
||||||
|
}
|
||||||
|
|
||||||
|
public OAuth2LoginConfigurer<B> and() {
|
||||||
|
return OAuth2LoginConfigurer.this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(B http) throws Exception {
|
||||||
|
this.authorizationCodeRequestRedirectFilterConfigurer.setBuilder(http);
|
||||||
|
this.authorizationCodeAuthenticationFilterConfigurer.setBuilder(http);
|
||||||
|
|
||||||
|
this.authorizationCodeRequestRedirectFilterConfigurer.init(http);
|
||||||
|
this.authorizationCodeAuthenticationFilterConfigurer.init(http);
|
||||||
|
this.initDefaultLoginFilter(http);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(B http) throws Exception {
|
||||||
|
this.authorizationCodeRequestRedirectFilterConfigurer.configure(http);
|
||||||
|
this.authorizationCodeAuthenticationFilterConfigurer.configure(http);
|
||||||
|
}
|
||||||
|
|
||||||
|
static <B extends HttpSecurityBuilder<B>> ClientRegistrationRepository getClientRegistrationRepository(B http) {
|
||||||
|
ClientRegistrationRepository clientRegistrationRepository = http.getSharedObject(ClientRegistrationRepository.class);
|
||||||
|
if (clientRegistrationRepository == null) {
|
||||||
|
clientRegistrationRepository = getDefaultClientRegistrationRepository(http);
|
||||||
|
http.setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository);
|
||||||
|
}
|
||||||
|
return clientRegistrationRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <B extends HttpSecurityBuilder<B>> ClientRegistrationRepository getDefaultClientRegistrationRepository(B http) {
|
||||||
|
return http.getSharedObject(ApplicationContext.class).getBean(ClientRegistrationRepository.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initDefaultLoginFilter(B http) {
|
||||||
|
DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http.getSharedObject(DefaultLoginPageGeneratingFilter.class);
|
||||||
|
if (loginPageGeneratingFilter != null && !this.authorizationCodeAuthenticationFilterConfigurer.isCustomLoginPage()) {
|
||||||
|
ClientRegistrationRepository clientRegistrationRepository = getClientRegistrationRepository(this.getBuilder());
|
||||||
|
if (!CollectionUtils.isEmpty(clientRegistrationRepository.getRegistrations())) {
|
||||||
|
Map<String, String> oauth2AuthenticationUrlToClientName = clientRegistrationRepository.getRegistrations().stream()
|
||||||
|
.collect(Collectors.toMap(e -> AuthorizationCodeRequestRedirectFilter.AUTHORIZATION_BASE_URI + "/" + e.getClientAlias(),
|
||||||
|
e -> e.getClientName()));
|
||||||
|
loginPageGeneratingFilter.setOauth2LoginEnabled(true);
|
||||||
|
loginPageGeneratingFilter.setOauth2AuthenticationUrlToClientName(oauth2AuthenticationUrlToClientName);
|
||||||
|
loginPageGeneratingFilter.setLoginPageUrl(this.authorizationCodeAuthenticationFilterConfigurer.getLoginUrl());
|
||||||
|
loginPageGeneratingFilter.setFailureUrl(this.authorizationCodeAuthenticationFilterConfigurer.getLoginFailureUrl());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -78,7 +78,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
|
||||||
springSecurityFilterChain.doFilter(request,response,chain)
|
springSecurityFilterChain.doFilter(request,response,chain)
|
||||||
then:
|
then:
|
||||||
response.getContentAsString() == """<html><head><title>Login Page</title></head><body onload='document.f.username.focus();'>
|
response.getContentAsString() == """<html><head><title>Login Page</title></head><body onload='document.f.username.focus();'>
|
||||||
<p><font color='red'>Your login attempt was not successful, try again.<br/><br/>Reason: Bad credentials</font></p><h3>Login with Username and Password</h3><form name='f' action='/login' method='POST'>
|
<p style='color:red;'>Your login attempt was not successful, try again.<br/><br/>Reason: Bad credentials</p><h3>Login with Username and Password</h3><form name='f' action='/login' method='POST'>
|
||||||
<table>
|
<table>
|
||||||
<tr><td>User:</td><td><input type='text' name='username' value=''></td></tr>
|
<tr><td>User:</td><td><input type='text' name='username' value=''></td></tr>
|
||||||
<tr><td>Password:</td><td><input type='password' name='password'/></td></tr>
|
<tr><td>Password:</td><td><input type='password' name='password'/></td></tr>
|
||||||
|
@ -107,7 +107,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
|
||||||
springSecurityFilterChain.doFilter(request,response,chain)
|
springSecurityFilterChain.doFilter(request,response,chain)
|
||||||
then: "sent to default success page"
|
then: "sent to default success page"
|
||||||
response.getContentAsString() == """<html><head><title>Login Page</title></head><body onload='document.f.username.focus();'>
|
response.getContentAsString() == """<html><head><title>Login Page</title></head><body onload='document.f.username.focus();'>
|
||||||
<p><font color='green'>You have been logged out</font></p><h3>Login with Username and Password</h3><form name='f' action='/login' method='POST'>
|
<p style='color:green;'>You have been logged out</p><h3>Login with Username and Password</h3><form name='f' action='/login' method='POST'>
|
||||||
<table>
|
<table>
|
||||||
<tr><td>User:</td><td><input type='text' name='username' value=''></td></tr>
|
<tr><td>User:</td><td><input type='text' name='username' value=''></td></tr>
|
||||||
<tr><td>Password:</td><td><input type='password' name='password'/></td></tr>
|
<tr><td>Password:</td><td><input type='password' name='password'/></td></tr>
|
||||||
|
|
|
@ -375,42 +375,15 @@ git clone https://github.com/spring-projects/spring-security.git
|
||||||
This will give you access to the entire project history (including all releases and branches) on your local machine.
|
This will give you access to the entire project history (including all releases and branches) on your local machine.
|
||||||
|
|
||||||
[[new]]
|
[[new]]
|
||||||
== What's New in Spring Security 4.2
|
== What's New in Spring Security 5.0
|
||||||
|
|
||||||
Among other things, Spring Security 4.2 brings early support for Spring Framework 5.
|
Spring Security 5.0 provides a number of new features as well as support for Spring Framework 5.
|
||||||
You can find the change logs for https://github.com/spring-projects/spring-security/milestone/86?closed=1[4.2.0.M1], https://github.com/spring-projects/spring-security/milestone/91?closed=1[4.2.0.RC1], https://github.com/spring-projects/spring-security/milestone/92?closed=1[4.2.0.RELEASE] which closes over 80 issues.
|
You can find the change log at https://github.com/spring-projects/spring-security/milestone/90?closed=1[5.0.0.M1].
|
||||||
The overwhelming majority of these features were contributed by the community.
|
Below are the highlights of this milestone release.
|
||||||
Below you can find the highlights of this release.
|
|
||||||
|
|
||||||
=== Web Improvements
|
=== New Features
|
||||||
|
|
||||||
* https://github.com/spring-projects/spring-security/pull/3812[#3812] - <<jackson,Jackson Support>>
|
|
||||||
* https://github.com/spring-projects/spring-security/pull/4116[#4116] - <<headers-referrer,Referrer Policy>>
|
|
||||||
* https://github.com/spring-projects/spring-security/pull/3938[#3938] - Add <<request-matching,HTTP response splitting prevention>>
|
|
||||||
* https://github.com/spring-projects/spring-security/issues/3949[#3949] - Add <<mvc-authentication-principal,bean reference support to @AuthenticationPrincipal>>.
|
|
||||||
* https://github.com/spring-projects/spring-security/pull/3978[#3978] - Support for Standford WebAuth and Shibboleth using the newly added http://docs.spring.io/spring-security/site/docs/4.2.x-SNAPSHOT/apidocs/org/springframework/security/web/authentication/preauth/RequestAttributeAuthenticationFilter.html[RequestAttributeAuthenticationFilter].
|
|
||||||
* https://github.com/spring-projects/spring-security/issues/4076[#4076] - Document <<appendix-proxy-server,Proxy Server>> Configuration
|
|
||||||
* https://github.com/spring-projects/spring-security/issues/3795[#3795] - `ConcurrentSessionFilter` supports `InvalidSessionStrategy`
|
|
||||||
* https://github.com/spring-projects/spring-security/pull/3904[#3904] - Add `CompositeLogoutHandler`
|
|
||||||
|
|
||||||
=== Configuration Improvements
|
|
||||||
|
|
||||||
* https://github.com/spring-projects/spring-security/pull/3956[#3956] - Central configuration of the http://docs.spring.io/spring-security/site/migrate/current/3-to-4/html5/migrate-3-to-4-jc.html#m3to4-role-prefixing[default role prefix]. See the issue for details.
|
|
||||||
* https://github.com/spring-projects/spring-security/issues/4102[#4102] - Custom default configuration in `WebSecurityConfigurerAdapter`. See <<jc-custom-dsls>>
|
|
||||||
* https://github.com/spring-projects/spring-security/issues/3899[#3899] - <<nsa-concurrency-control-max-sessions,concurrency-control@max-sessions>> supports unlimited sessions.
|
|
||||||
* https://github.com/spring-projects/spring-security/issues/4097[#4097] - <<nsa-intercept-url-request-matcher-ref,intercept-url@request-matcher-ref>> adds more powerful request matching support to the XML namespace.
|
|
||||||
* https://github.com/spring-projects/spring-security/issues/3990[#3990] - Support for constructing `RoleHierarchy` from `Map` (i.e. `yml`)
|
|
||||||
* https://github.com/spring-projects/spring-security/pull/4062[#4062] - Custom cookiePath to `CookieCsrfTokenRepository`
|
|
||||||
* https://github.com/spring-projects/spring-security/issues/3794[#3794] - Allow configuration of `InvalidSessionStrategy` on `SessionManagementConfigurer`
|
|
||||||
* https://github.com/spring-projects/spring-security/issues/4020[#4020] - Fix Exposing Beans for defaultMethodExpressionHandler can prevent Method Security
|
|
||||||
|
|
||||||
=== Miscellaneous
|
|
||||||
|
|
||||||
* https://github.com/spring-projects/spring-security/issues/4080[#4080] - Spring 5 support
|
|
||||||
* https://github.com/spring-projects/spring-security/issues/4095[#4095] - `Add UserBuilder`
|
|
||||||
* https://github.com/spring-projects/spring-security/issues/4018[#4018] - Fix after `csrf()` is invoked, future `MockMvc` invocations use original `CsrfTokenRepository`
|
|
||||||
* Version Updates
|
|
||||||
|
|
||||||
|
* 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]]
|
||||||
== Samples and Guides (Start Here)
|
== Samples and Guides (Start Here)
|
||||||
|
|
|
@ -0,0 +1,156 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-oauth2-client</artifactId>
|
||||||
|
<version>5.0.0.BUILD-SNAPSHOT</version>
|
||||||
|
<name>spring-security-oauth2-client</name>
|
||||||
|
<description>spring-security-oauth2-client</description>
|
||||||
|
<url>http://spring.io/spring-security</url>
|
||||||
|
<organization>
|
||||||
|
<name>spring.io</name>
|
||||||
|
<url>http://spring.io/</url>
|
||||||
|
</organization>
|
||||||
|
<licenses>
|
||||||
|
<license>
|
||||||
|
<name>The Apache Software License, Version 2.0</name>
|
||||||
|
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
|
||||||
|
<distribution>repo</distribution>
|
||||||
|
</license>
|
||||||
|
</licenses>
|
||||||
|
<developers>
|
||||||
|
<developer>
|
||||||
|
<id>rwinch</id>
|
||||||
|
<name>Rob Winch</name>
|
||||||
|
<email>rwinch@pivotal.io</email>
|
||||||
|
</developer>
|
||||||
|
<developer>
|
||||||
|
<id>jgrandja</id>
|
||||||
|
<name>Joe Grandja</name>
|
||||||
|
<email>jgrandja@pivotal.io</email>
|
||||||
|
</developer>
|
||||||
|
</developers>
|
||||||
|
<scm>
|
||||||
|
<connection>scm:git:git://github.com/spring-projects/spring-security</connection>
|
||||||
|
<developerConnection>scm:git:git://github.com/spring-projects/spring-security</developerConnection>
|
||||||
|
<url>https://github.com/spring-projects/spring-security</url>
|
||||||
|
</scm>
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework</groupId>
|
||||||
|
<artifactId>spring-framework-bom</artifactId>
|
||||||
|
<version>4.3.5.RELEASE</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.nimbusds</groupId>
|
||||||
|
<artifactId>oauth2-oidc-sdk</artifactId>
|
||||||
|
<version>5.21</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-core</artifactId>
|
||||||
|
<version>5.0.0.BUILD-SNAPSHOT</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-oauth2-core</artifactId>
|
||||||
|
<version>5.0.0.BUILD-SNAPSHOT</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-web</artifactId>
|
||||||
|
<version>5.0.0.BUILD-SNAPSHOT</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework</groupId>
|
||||||
|
<artifactId>spring-core</artifactId>
|
||||||
|
<scope>compile</scope>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<artifactId>commons-logging</artifactId>
|
||||||
|
<groupId>commons-logging</groupId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework</groupId>
|
||||||
|
<artifactId>spring-web</artifactId>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>commons-logging</groupId>
|
||||||
|
<artifactId>commons-logging</artifactId>
|
||||||
|
<version>1.2</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>javax.servlet</groupId>
|
||||||
|
<artifactId>javax.servlet-api</artifactId>
|
||||||
|
<version>3.1.0</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>ch.qos.logback</groupId>
|
||||||
|
<artifactId>logback-classic</artifactId>
|
||||||
|
<version>1.1.2</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<version>4.12</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.assertj</groupId>
|
||||||
|
<artifactId>assertj-core</artifactId>
|
||||||
|
<version>3.6.2</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-core</artifactId>
|
||||||
|
<version>1.10.19</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>jcl-over-slf4j</artifactId>
|
||||||
|
<version>1.7.7</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework</groupId>
|
||||||
|
<artifactId>spring-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>spring-snapshot</id>
|
||||||
|
<url>https://repo.spring.io/snapshot</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<source>1.8</source>
|
||||||
|
<target>1.8</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
|
@ -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'
|
||||||
|
}
|
|
@ -0,0 +1,225 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client.authentication;
|
||||||
|
|
||||||
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||||
|
import org.springframework.security.oauth2.client.user.OAuth2UserService;
|
||||||
|
import org.springframework.security.oauth2.client.web.converter.AuthorizationCodeAuthorizationResponseAttributesConverter;
|
||||||
|
import org.springframework.security.oauth2.client.web.converter.ErrorResponseAttributesConverter;
|
||||||
|
import org.springframework.security.oauth2.core.AccessToken;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.*;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
|
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
|
||||||
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
|
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static org.springframework.security.oauth2.client.authentication.AuthorizationCodeRequestRedirectFilter.isDefaultRedirectUri;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of an {@link AbstractAuthenticationProcessingFilter} that handles
|
||||||
|
* the processing of an <i>OAuth 2.0 Authorization Response</i> for the authorization code grant flow.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This <code>Filter</code> processes the <i>Authorization Response</i> in the following step sequence:
|
||||||
|
*
|
||||||
|
* <ol>
|
||||||
|
* <li>
|
||||||
|
* Assuming the resource owner (end-user) has granted access to the client, the authorization server will append the
|
||||||
|
* {@link OAuth2Parameter#CODE} and {@link OAuth2Parameter#STATE} (if provided in the <i>Authorization Request</i>) parameters
|
||||||
|
* to the {@link OAuth2Parameter#REDIRECT_URI} (provided in the <i>Authorization Request</i>)
|
||||||
|
* and redirect the end-user's user-agent back to this <code>Filter</code> (the client).
|
||||||
|
* </li>
|
||||||
|
* <li>
|
||||||
|
* This <code>Filter</code> will then create an {@link AuthorizationCodeAuthenticationToken} with
|
||||||
|
* the {@link OAuth2Parameter#CODE} received in the previous step and pass it to
|
||||||
|
* {@link AuthorizationCodeAuthenticationProvider#authenticate(Authentication)} (indirectly via {@link AuthenticationManager}).
|
||||||
|
* The {@link AuthorizationCodeAuthenticationProvider} will use an {@link AuthorizationGrantTokenExchanger} to make a request
|
||||||
|
* to the authorization server's <i>Token Endpoint</i> for exchanging the {@link OAuth2Parameter#CODE} for an {@link AccessToken}.
|
||||||
|
* </li>
|
||||||
|
* <li>
|
||||||
|
* Upon receiving the <i>Access Token Request</i>, the authorization server will authenticate the client,
|
||||||
|
* verify the {@link OAuth2Parameter#CODE}, and ensure that the {@link OAuth2Parameter#REDIRECT_URI}
|
||||||
|
* received matches the <code>URI</code> originally provided in the <i>Authorization Request</i>.
|
||||||
|
* If the request is valid, the authorization server will respond back with a {@link TokenResponseAttributes}.
|
||||||
|
* </li>
|
||||||
|
* <li>
|
||||||
|
* The {@link AuthorizationCodeAuthenticationProvider} will then create a new {@link OAuth2AuthenticationToken}
|
||||||
|
* associating the {@link AccessToken} from the {@link TokenResponseAttributes} and pass it to
|
||||||
|
* {@link OAuth2UserService#loadUser(OAuth2AuthenticationToken)}. The {@link OAuth2UserService} will make a request
|
||||||
|
* to the authorization server's <i>UserInfo Endpoint</i> (using the {@link AccessToken})
|
||||||
|
* to obtain the end-user's (resource owner) attributes and return it in the form of an {@link OAuth2User}.
|
||||||
|
* </li>
|
||||||
|
* <li>
|
||||||
|
* The {@link AuthorizationCodeAuthenticationProvider} will create another new {@link OAuth2AuthenticationToken}
|
||||||
|
* but this time associating the {@link AccessToken} and {@link OAuth2User} returned from the {@link OAuth2UserService}.
|
||||||
|
* Finally, the {@link OAuth2AuthenticationToken} is returned to the {@link AuthenticationManager}
|
||||||
|
* and then back to this <code>Filter</code> at which point the session is considered <i>"authenticated"</i>.
|
||||||
|
* </li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* <b>NOTE:</b> Steps 4-5 are <b>not</b> part of the authorization code grant flow and instead are
|
||||||
|
* <i>"authentication flow"</i> steps that are required in order to authenticate the end-user with the system.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
* @see AbstractAuthenticationProcessingFilter
|
||||||
|
* @see AuthorizationCodeAuthenticationToken
|
||||||
|
* @see AuthorizationCodeAuthenticationProvider
|
||||||
|
* @see AuthorizationGrantTokenExchanger
|
||||||
|
* @see AuthorizationCodeAuthorizationResponseAttributes
|
||||||
|
* @see AuthorizationRequestAttributes
|
||||||
|
* @see AuthorizationRequestRepository
|
||||||
|
* @see AuthorizationCodeRequestRedirectFilter
|
||||||
|
* @see ClientRegistration
|
||||||
|
* @see ClientRegistrationRepository
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant Flow</a>
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.2">Section 4.1.2 Authorization Response</a>
|
||||||
|
*/
|
||||||
|
public class AuthorizationCodeAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {
|
||||||
|
public static final String AUTHORIZE_BASE_URI = "/oauth2/authorize/code";
|
||||||
|
private static final String CLIENT_ALIAS_VARIABLE_NAME = "clientAlias";
|
||||||
|
private static final String AUTHORIZE_URI = AUTHORIZE_BASE_URI + "/{" + CLIENT_ALIAS_VARIABLE_NAME + "}";
|
||||||
|
private static final String AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE = "authorization_request_not_found";
|
||||||
|
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
|
||||||
|
private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_parameter";
|
||||||
|
private final ErrorResponseAttributesConverter errorResponseConverter = new ErrorResponseAttributesConverter();
|
||||||
|
private final AuthorizationCodeAuthorizationResponseAttributesConverter authorizationCodeResponseConverter =
|
||||||
|
new AuthorizationCodeAuthorizationResponseAttributesConverter();
|
||||||
|
private final RequestMatcher authorizeRequestMatcher = new AntPathRequestMatcher(AUTHORIZE_URI);
|
||||||
|
private ClientRegistrationRepository clientRegistrationRepository;
|
||||||
|
private AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionAuthorizationRequestRepository();
|
||||||
|
|
||||||
|
public AuthorizationCodeAuthenticationProcessingFilter() {
|
||||||
|
super(AUTHORIZE_URI);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
|
||||||
|
throws AuthenticationException, IOException, ServletException {
|
||||||
|
|
||||||
|
ErrorResponseAttributes authorizationError = this.errorResponseConverter.convert(request);
|
||||||
|
if (authorizationError != null) {
|
||||||
|
OAuth2Error oauth2Error = new OAuth2Error(authorizationError.getErrorCode(),
|
||||||
|
authorizationError.getDescription(), authorizationError.getUri());
|
||||||
|
this.getAuthorizationRequestRepository().removeAuthorizationRequest(request);
|
||||||
|
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthorizationRequestAttributes matchingAuthorizationRequest = this.resolveAuthorizationRequest(request);
|
||||||
|
|
||||||
|
ClientRegistration clientRegistration = this.getClientRegistrationRepository().getRegistrationByClientId(
|
||||||
|
matchingAuthorizationRequest.getClientId());
|
||||||
|
|
||||||
|
// If clientRegistration.redirectUri is the default one (with Uri template variables)
|
||||||
|
// then use matchingAuthorizationRequest.redirectUri instead
|
||||||
|
if (isDefaultRedirectUri(clientRegistration)) {
|
||||||
|
clientRegistration = new ClientRegistrationBuilderWithUriOverrides(
|
||||||
|
clientRegistration, matchingAuthorizationRequest.getRedirectUri()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthorizationCodeAuthorizationResponseAttributes authorizationCodeResponseAttributes =
|
||||||
|
this.authorizationCodeResponseConverter.convert(request);
|
||||||
|
|
||||||
|
AuthorizationCodeAuthenticationToken authRequest = new AuthorizationCodeAuthenticationToken(
|
||||||
|
authorizationCodeResponseAttributes.getCode(), clientRegistration);
|
||||||
|
|
||||||
|
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
|
||||||
|
|
||||||
|
Authentication authenticated = this.getAuthenticationManager().authenticate(authRequest);
|
||||||
|
|
||||||
|
return authenticated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RequestMatcher getAuthorizeRequestMatcher() {
|
||||||
|
return this.authorizeRequestMatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ClientRegistrationRepository getClientRegistrationRepository() {
|
||||||
|
return this.clientRegistrationRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void setClientRegistrationRepository(ClientRegistrationRepository clientRegistrationRepository) {
|
||||||
|
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
|
||||||
|
Assert.notEmpty(clientRegistrationRepository.getRegistrations(), "clientRegistrationRepository cannot be empty");
|
||||||
|
this.clientRegistrationRepository = clientRegistrationRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected AuthorizationRequestRepository getAuthorizationRequestRepository() {
|
||||||
|
return this.authorizationRequestRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void setAuthorizationRequestRepository(AuthorizationRequestRepository authorizationRequestRepository) {
|
||||||
|
Assert.notNull(authorizationRequestRepository, "authorizationRequestRepository cannot be null");
|
||||||
|
this.authorizationRequestRepository = authorizationRequestRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AuthorizationRequestAttributes resolveAuthorizationRequest(HttpServletRequest request) {
|
||||||
|
AuthorizationRequestAttributes authorizationRequest =
|
||||||
|
this.getAuthorizationRequestRepository().loadAuthorizationRequest(request);
|
||||||
|
if (authorizationRequest == null) {
|
||||||
|
OAuth2Error oauth2Error = new OAuth2Error(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE);
|
||||||
|
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||||
|
}
|
||||||
|
this.getAuthorizationRequestRepository().removeAuthorizationRequest(request);
|
||||||
|
this.assertMatchingAuthorizationRequest(request, authorizationRequest);
|
||||||
|
return authorizationRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertMatchingAuthorizationRequest(HttpServletRequest request, AuthorizationRequestAttributes authorizationRequest) {
|
||||||
|
String state = request.getParameter(OAuth2Parameter.STATE);
|
||||||
|
if (!authorizationRequest.getState().equals(state)) {
|
||||||
|
OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE);
|
||||||
|
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!request.getRequestURL().toString().equals(authorizationRequest.getRedirectUri())) {
|
||||||
|
OAuth2Error oauth2Error = new OAuth2Error(INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE);
|
||||||
|
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ClientRegistrationBuilderWithUriOverrides extends ClientRegistration.Builder {
|
||||||
|
|
||||||
|
private ClientRegistrationBuilderWithUriOverrides(ClientRegistration clientRegistration, String redirectUri) {
|
||||||
|
super(clientRegistration.getClientId());
|
||||||
|
this.clientSecret(clientRegistration.getClientSecret());
|
||||||
|
this.clientAuthenticationMethod(clientRegistration.getClientAuthenticationMethod());
|
||||||
|
this.authorizedGrantType(clientRegistration.getAuthorizedGrantType());
|
||||||
|
this.redirectUri(redirectUri);
|
||||||
|
if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) {
|
||||||
|
this.scopes(clientRegistration.getScopes().stream().toArray(String[]::new));
|
||||||
|
}
|
||||||
|
this.authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri());
|
||||||
|
this.tokenUri(clientRegistration.getProviderDetails().getTokenUri());
|
||||||
|
this.userInfoUri(clientRegistration.getProviderDetails().getUserInfoUri());
|
||||||
|
this.clientName(clientRegistration.getClientName());
|
||||||
|
this.clientAlias(clientRegistration.getClientAlias());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,120 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client.authentication;
|
||||||
|
|
||||||
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
|
import org.springframework.security.authentication.AuthenticationProvider;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
||||||
|
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
|
||||||
|
import org.springframework.security.oauth2.client.user.OAuth2UserService;
|
||||||
|
import org.springframework.security.oauth2.core.AccessToken;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.TokenResponseAttributes;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of an {@link AuthenticationProvider} that is responsible for authenticating
|
||||||
|
* an <i>authorization code</i> credential with the authorization server's <i>Token Endpoint</i>
|
||||||
|
* and if valid, exchanging it for an <i>access token</i> credential.
|
||||||
|
* Additionally, it will also obtain the end-user's (resource owner) attributes from the <i>UserInfo Endpoint</i>
|
||||||
|
* (using the <i>access token</i>) and create a <code>Principal</code> in the form of an {@link OAuth2User}
|
||||||
|
* associating it with the returned {@link OAuth2AuthenticationToken}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The {@link AuthorizationCodeAuthenticationProvider} uses an {@link AuthorizationGrantTokenExchanger}
|
||||||
|
* to make a request to the authorization server's <i>Token Endpoint</i>
|
||||||
|
* to verify the {@link AuthorizationCodeAuthenticationToken#getAuthorizationCode()}.
|
||||||
|
* If the request is valid, the authorization server will respond back with a {@link TokenResponseAttributes}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* It will then create a {@link OAuth2AuthenticationToken} associating the {@link AccessToken}
|
||||||
|
* from the {@link TokenResponseAttributes} and pass it to {@link OAuth2UserService#loadUser(OAuth2AuthenticationToken)}
|
||||||
|
* to obtain the end-user's (resource owner) attributes in the form of an {@link OAuth2User}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Finally, it will create another {@link OAuth2AuthenticationToken}, this time associating
|
||||||
|
* the {@link AccessToken} and {@link OAuth2User} and return it to the {@link AuthenticationManager},
|
||||||
|
* at which point the {@link OAuth2AuthenticationToken} is considered <i>"authenticated"</i>.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
* @see AuthorizationCodeAuthenticationToken
|
||||||
|
* @see AuthorizationGrantTokenExchanger
|
||||||
|
* @see TokenResponseAttributes
|
||||||
|
* @see AccessToken
|
||||||
|
* @see OAuth2UserService
|
||||||
|
* @see OAuth2User
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant Flow</a>
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request</a>
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response</a>
|
||||||
|
*/
|
||||||
|
public class AuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
|
||||||
|
private final AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> authorizationCodeTokenExchanger;
|
||||||
|
private final OAuth2UserService userInfoService;
|
||||||
|
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
|
||||||
|
|
||||||
|
public AuthorizationCodeAuthenticationProvider(
|
||||||
|
AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> authorizationCodeTokenExchanger,
|
||||||
|
OAuth2UserService userInfoService) {
|
||||||
|
|
||||||
|
Assert.notNull(authorizationCodeTokenExchanger, "authorizationCodeTokenExchanger cannot be null");
|
||||||
|
Assert.notNull(userInfoService, "userInfoService cannot be null");
|
||||||
|
this.authorizationCodeTokenExchanger = authorizationCodeTokenExchanger;
|
||||||
|
this.userInfoService = userInfoService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||||
|
AuthorizationCodeAuthenticationToken authorizationCodeAuthentication =
|
||||||
|
(AuthorizationCodeAuthenticationToken) authentication;
|
||||||
|
|
||||||
|
TokenResponseAttributes tokenResponse =
|
||||||
|
this.authorizationCodeTokenExchanger.exchange(authorizationCodeAuthentication);
|
||||||
|
|
||||||
|
AccessToken accessToken = new AccessToken(tokenResponse.getTokenType(),
|
||||||
|
tokenResponse.getTokenValue(), tokenResponse.getIssuedAt(),
|
||||||
|
tokenResponse.getExpiresAt(), tokenResponse.getScopes());
|
||||||
|
OAuth2AuthenticationToken accessTokenAuthentication = new OAuth2AuthenticationToken(
|
||||||
|
authorizationCodeAuthentication.getClientRegistration(), accessToken);
|
||||||
|
accessTokenAuthentication.setDetails(authorizationCodeAuthentication.getDetails());
|
||||||
|
|
||||||
|
OAuth2User user = this.userInfoService.loadUser(accessTokenAuthentication);
|
||||||
|
|
||||||
|
Collection<? extends GrantedAuthority> authorities =
|
||||||
|
this.authoritiesMapper.mapAuthorities(user.getAuthorities());
|
||||||
|
|
||||||
|
OAuth2AuthenticationToken authenticationResult = new OAuth2AuthenticationToken(user, authorities,
|
||||||
|
accessTokenAuthentication.getClientRegistration(), accessTokenAuthentication.getAccessToken());
|
||||||
|
authenticationResult.setDetails(accessTokenAuthentication.getDetails());
|
||||||
|
|
||||||
|
return authenticationResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(Class<?> authentication) {
|
||||||
|
return AuthorizationCodeAuthenticationToken.class.isAssignableFrom(authentication);
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
|
||||||
|
Assert.notNull(authoritiesMapper, "authoritiesMapper cannot be null");
|
||||||
|
this.authoritiesMapper = authoritiesMapper;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client.authentication;
|
||||||
|
|
||||||
|
import org.springframework.security.core.authority.AuthorityUtils;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of an {@link AuthorizationGrantAuthenticationToken} that holds
|
||||||
|
* an <i>authorization code grant</i> credential for a specific client identified in {@link #getClientRegistration()}.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
* @see AuthorizationGrantAuthenticationToken
|
||||||
|
* @see ClientRegistration
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3.1">Section 1.3.1 Authorization Code Grant</a>
|
||||||
|
*/
|
||||||
|
public class AuthorizationCodeAuthenticationToken extends AuthorizationGrantAuthenticationToken {
|
||||||
|
private final String authorizationCode;
|
||||||
|
private final ClientRegistration clientRegistration;
|
||||||
|
|
||||||
|
public AuthorizationCodeAuthenticationToken(String authorizationCode, ClientRegistration clientRegistration) {
|
||||||
|
super(AuthorizationGrantType.AUTHORIZATION_CODE, AuthorityUtils.NO_AUTHORITIES);
|
||||||
|
Assert.hasText(authorizationCode, "authorizationCode cannot be empty");
|
||||||
|
Assert.notNull(clientRegistration, "clientRegistration cannot be null");
|
||||||
|
this.authorizationCode = authorizationCode;
|
||||||
|
this.clientRegistration = clientRegistration;
|
||||||
|
this.setAuthenticated(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getPrincipal() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getCredentials() {
|
||||||
|
return this.getAuthorizationCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAuthorizationCode() {
|
||||||
|
return this.authorizationCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientRegistration getClientRegistration() {
|
||||||
|
return this.clientRegistration;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,159 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client.authentication;
|
||||||
|
|
||||||
|
import org.springframework.security.crypto.keygen.StringKeyGenerator;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.AuthorizationRequestAttributes;
|
||||||
|
import org.springframework.security.web.DefaultRedirectStrategy;
|
||||||
|
import org.springframework.security.web.RedirectStrategy;
|
||||||
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
|
import javax.servlet.FilterChain;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
import static org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationProcessingFilter.AUTHORIZE_BASE_URI;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This <code>Filter</code> initiates the authorization code grant flow by redirecting
|
||||||
|
* the end-user's user-agent to the authorization server's <i>Authorization Endpoint</i>.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* It uses an {@link AuthorizationRequestUriBuilder} to build the <i>OAuth 2.0 Authorization Request</i>,
|
||||||
|
* which is used as the redirect <code>URI</code> to the <i>Authorization Endpoint</i>.
|
||||||
|
* The redirect <code>URI</code> will include the client identifier, requested scope(s), state, response type, and a redirection URI
|
||||||
|
* which the authorization server will send the user-agent back to (handled by {@link AuthorizationCodeAuthenticationProcessingFilter})
|
||||||
|
* once access is granted (or denied) by the end-user (resource owner).
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
* @see AuthorizationRequestAttributes
|
||||||
|
* @see AuthorizationRequestRepository
|
||||||
|
* @see AuthorizationRequestUriBuilder
|
||||||
|
* @see ClientRegistration
|
||||||
|
* @see ClientRegistrationRepository
|
||||||
|
* @see AuthorizationCodeAuthenticationProcessingFilter
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant Flow</a>
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.1">Section 4.1.1 Authorization Request</a>
|
||||||
|
*/
|
||||||
|
public class AuthorizationCodeRequestRedirectFilter extends OncePerRequestFilter {
|
||||||
|
public static final String AUTHORIZATION_BASE_URI = "/oauth2/authorization/code";
|
||||||
|
private static final String CLIENT_ALIAS_VARIABLE_NAME = "clientAlias";
|
||||||
|
private static final String AUTHORIZATION_URI = AUTHORIZATION_BASE_URI + "/{" + CLIENT_ALIAS_VARIABLE_NAME + "}";
|
||||||
|
private static final String DEFAULT_REDIRECT_URI_TEMPLATE = "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}";
|
||||||
|
private final AntPathRequestMatcher authorizationRequestMatcher;
|
||||||
|
private final ClientRegistrationRepository clientRegistrationRepository;
|
||||||
|
private final AuthorizationRequestUriBuilder authorizationUriBuilder;
|
||||||
|
private final RedirectStrategy authorizationRedirectStrategy = new DefaultRedirectStrategy();
|
||||||
|
private final StringKeyGenerator stateGenerator = new DefaultStateGenerator();
|
||||||
|
private AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionAuthorizationRequestRepository();
|
||||||
|
|
||||||
|
public AuthorizationCodeRequestRedirectFilter(ClientRegistrationRepository clientRegistrationRepository,
|
||||||
|
AuthorizationRequestUriBuilder authorizationUriBuilder) {
|
||||||
|
|
||||||
|
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
|
||||||
|
Assert.notNull(authorizationUriBuilder, "authorizationUriBuilder cannot be null");
|
||||||
|
this.authorizationRequestMatcher = new AntPathRequestMatcher(AUTHORIZATION_URI);
|
||||||
|
this.clientRegistrationRepository = clientRegistrationRepository;
|
||||||
|
this.authorizationUriBuilder = authorizationUriBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void setAuthorizationRequestRepository(AuthorizationRequestRepository authorizationRequestRepository) {
|
||||||
|
Assert.notNull(authorizationRequestRepository, "authorizationRequestRepository cannot be null");
|
||||||
|
this.authorizationRequestRepository = authorizationRequestRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
|
||||||
|
if (this.requiresAuthorization(request, response)) {
|
||||||
|
try {
|
||||||
|
this.sendRedirectForAuthorization(request, response);
|
||||||
|
} catch (Exception failed) {
|
||||||
|
this.unsuccessfulAuthorization(request, response, failed);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean requiresAuthorization(HttpServletRequest request, HttpServletResponse response) {
|
||||||
|
return this.authorizationRequestMatcher.matches(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
|
||||||
|
String clientAlias = this.authorizationRequestMatcher
|
||||||
|
.extractUriTemplateVariables(request).get(CLIENT_ALIAS_VARIABLE_NAME);
|
||||||
|
ClientRegistration clientRegistration = this.clientRegistrationRepository.getRegistrationByClientAlias(clientAlias);
|
||||||
|
if (clientRegistration == null) {
|
||||||
|
throw new IllegalArgumentException("Invalid Client Identifier (Alias): " + clientAlias);
|
||||||
|
}
|
||||||
|
|
||||||
|
String redirectUriStr;
|
||||||
|
if (isDefaultRedirectUri(clientRegistration)) {
|
||||||
|
redirectUriStr = this.expandDefaultRedirectUri(request, clientRegistration);
|
||||||
|
} else {
|
||||||
|
redirectUriStr = clientRegistration.getRedirectUri();
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthorizationRequestAttributes authorizationRequestAttributes =
|
||||||
|
AuthorizationRequestAttributes.withAuthorizationCode()
|
||||||
|
.clientId(clientRegistration.getClientId())
|
||||||
|
.authorizeUri(clientRegistration.getProviderDetails().getAuthorizationUri())
|
||||||
|
.redirectUri(redirectUriStr)
|
||||||
|
.scopes(clientRegistration.getScopes())
|
||||||
|
.state(this.stateGenerator.generateKey())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequestAttributes, request);
|
||||||
|
|
||||||
|
URI redirectUri = this.authorizationUriBuilder.build(authorizationRequestAttributes);
|
||||||
|
this.authorizationRedirectStrategy.sendRedirect(request, response, redirectUri.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void unsuccessfulAuthorization(HttpServletRequest request, HttpServletResponse response,
|
||||||
|
Exception failed) throws IOException, ServletException {
|
||||||
|
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("Authorization Request failed: " + failed.toString(), failed);
|
||||||
|
}
|
||||||
|
response.sendError(HttpServletResponse.SC_BAD_REQUEST, failed.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isDefaultRedirectUri(ClientRegistration clientRegistration) {
|
||||||
|
return DEFAULT_REDIRECT_URI_TEMPLATE.equals(clientRegistration.getRedirectUri());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String expandDefaultRedirectUri(HttpServletRequest request, ClientRegistration clientRegistration) {
|
||||||
|
return UriComponentsBuilder.fromUriString(DEFAULT_REDIRECT_URI_TEMPLATE)
|
||||||
|
.buildAndExpand(request.getScheme(), request.getServerName(), request.getServerPort(),
|
||||||
|
AUTHORIZE_BASE_URI, clientRegistration.getClientAlias())
|
||||||
|
.encode()
|
||||||
|
.toUriString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client.authentication;
|
||||||
|
|
||||||
|
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.SpringSecurityCoreVersion;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base implementation of an {@link AbstractAuthenticationToken} that holds
|
||||||
|
* an <i>authorization grant</i> credential for a specific {@link AuthorizationGrantType}.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
* @see AuthorizationGrantType
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3">Section 1.3 Authorization Grant</a>
|
||||||
|
*/
|
||||||
|
public abstract class AuthorizationGrantAuthenticationToken extends AbstractAuthenticationToken {
|
||||||
|
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
|
||||||
|
private final AuthorizationGrantType authorizationGrantType;
|
||||||
|
|
||||||
|
protected AuthorizationGrantAuthenticationToken(AuthorizationGrantType authorizationGrantType,
|
||||||
|
Collection<? extends GrantedAuthority> authorities) {
|
||||||
|
|
||||||
|
super(authorities);
|
||||||
|
Assert.notNull(authorizationGrantType, "authorizationGrantType cannot be null");
|
||||||
|
this.authorizationGrantType = authorizationGrantType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuthorizationGrantType getGrantType() {
|
||||||
|
return this.authorizationGrantType;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client.authentication;
|
||||||
|
|
||||||
|
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.TokenResponseAttributes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementations of this interface are responsible for <i>"exchanging"</i>
|
||||||
|
* an <i>authorization grant</i> credential (for example, an authorization code) for an
|
||||||
|
* <i>access token</i> credential at the authorization server's <i>Token Endpoint</i>.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
* @see AuthorizationGrantType
|
||||||
|
* @see AuthorizationGrantAuthenticationToken
|
||||||
|
* @see TokenResponseAttributes
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3">Section 1.3 Authorization Grant</a>
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request (Authorization Code Grant)</a>
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response (Authorization Code Grant)</a>
|
||||||
|
*/
|
||||||
|
public interface AuthorizationGrantTokenExchanger<T extends AuthorizationGrantAuthenticationToken> {
|
||||||
|
|
||||||
|
TokenResponseAttributes exchange(T authorizationGrantAuthentication) throws OAuth2AuthenticationException;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client.authentication;
|
||||||
|
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.AuthorizationRequestAttributes;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementations of this interface are responsible for the persistence
|
||||||
|
* of {@link AuthorizationRequestAttributes} between requests.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Used by the {@link AuthorizationCodeRequestRedirectFilter} for persisting the <i>Authorization Request</i>
|
||||||
|
* before it initiates the authorization code grant flow.
|
||||||
|
* As well, used by the {@link AuthorizationCodeAuthenticationProcessingFilter} when resolving
|
||||||
|
* the associated <i>Authorization Request</i> during the handling of the <i>Authorization Response</i>.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
* @see AuthorizationRequestAttributes
|
||||||
|
* @see HttpSessionAuthorizationRequestRepository
|
||||||
|
*/
|
||||||
|
public interface AuthorizationRequestRepository {
|
||||||
|
|
||||||
|
AuthorizationRequestAttributes loadAuthorizationRequest(HttpServletRequest request);
|
||||||
|
|
||||||
|
void saveAuthorizationRequest(AuthorizationRequestAttributes authorizationRequest, HttpServletRequest request);
|
||||||
|
|
||||||
|
AuthorizationRequestAttributes removeAuthorizationRequest(HttpServletRequest request);
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client.authentication;
|
||||||
|
|
||||||
|
import org.springframework.security.crypto.codec.Base64;
|
||||||
|
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
|
||||||
|
import org.springframework.security.crypto.keygen.KeyGenerators;
|
||||||
|
import org.springframework.security.crypto.keygen.StringKeyGenerator;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default implementation for generating the
|
||||||
|
* {@link org.springframework.security.oauth2.core.endpoint.OAuth2Parameter#STATE} parameter
|
||||||
|
* used in the <i>Authorization Request</i> and correlated in the <i>Authorization Response</i> (or <i>Error Response</i>).
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* <b>NOTE:</b> The value of the <i>state</i> parameter is an opaque <code>String</code>
|
||||||
|
* used by the client to prevent cross-site request forgery, as described in
|
||||||
|
* <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-10.12">Section 10.12</a> of the specification.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
*/
|
||||||
|
public class DefaultStateGenerator implements StringKeyGenerator {
|
||||||
|
private static final int DEFAULT_BYTE_LENGTH = 32;
|
||||||
|
private final BytesKeyGenerator keyGenerator;
|
||||||
|
|
||||||
|
public DefaultStateGenerator() {
|
||||||
|
this(DEFAULT_BYTE_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DefaultStateGenerator(int byteLength) {
|
||||||
|
Assert.isTrue(byteLength > 0, "byteLength must be greater than 0");
|
||||||
|
this.keyGenerator = KeyGenerators.secureRandom(byteLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String generateKey() {
|
||||||
|
return new String(Base64.encode(keyGenerator.generateKey()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client.authentication;
|
||||||
|
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This exception is thrown for all <i>OAuth 2.0</i> related {@link Authentication} errors.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* There are a number of scenarios where an error may occur, for example:
|
||||||
|
* <ul>
|
||||||
|
* <li>The authorization request or token request is missing a required parameter</li>
|
||||||
|
* <li>Missing or invalid client identifier</li>
|
||||||
|
* <li>Invalid or mismatching redirection URI</li>
|
||||||
|
* <li>The requested scope is invalid, unknown, or malformed</li>
|
||||||
|
* <li>The resource owner or authorization server denied the access request</li>
|
||||||
|
* <li>Client authentication failed</li>
|
||||||
|
* <li>The provided authorization grant (authorization code, resource owner credentials) is invalid, expired, or revoked</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
*/
|
||||||
|
public class OAuth2AuthenticationException extends AuthenticationException {
|
||||||
|
private OAuth2Error errorObject;
|
||||||
|
|
||||||
|
public OAuth2AuthenticationException(OAuth2Error errorObject, Throwable cause) {
|
||||||
|
this(errorObject, cause.getMessage(), cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OAuth2AuthenticationException(OAuth2Error errorObject, String message) {
|
||||||
|
super(message);
|
||||||
|
this.setErrorObject(errorObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OAuth2AuthenticationException(OAuth2Error errorObject, String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
this.setErrorObject(errorObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OAuth2Error getErrorObject() {
|
||||||
|
return errorObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setErrorObject(OAuth2Error errorObject) {
|
||||||
|
Assert.notNull(errorObject, "OAuth2 Error object cannot be null");
|
||||||
|
this.errorObject = errorObject;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client.authentication;
|
||||||
|
|
||||||
|
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.SpringSecurityCoreVersion;
|
||||||
|
import org.springframework.security.core.authority.AuthorityUtils;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.client.user.OAuth2UserService;
|
||||||
|
import org.springframework.security.oauth2.core.AccessToken;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of an {@link AbstractAuthenticationToken}
|
||||||
|
* that represents an <i>OAuth 2.0</i> {@link Authentication}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* It associates an {@link OAuth2User}, {@link ClientRegistration} and an {@link AccessToken}.
|
||||||
|
* This <code>Authentication</code> is considered <i>"authenticated"</i> if the {@link OAuth2User}
|
||||||
|
* is provided in the respective constructor. This typically happens after the {@link OAuth2UserService}
|
||||||
|
* retrieves the end-user's (resource owner) attributes from the <i>UserInfo Endpoint</i>.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
* @see OAuth2User
|
||||||
|
* @see ClientRegistration
|
||||||
|
* @see AccessToken
|
||||||
|
*/
|
||||||
|
public class OAuth2AuthenticationToken extends AbstractAuthenticationToken {
|
||||||
|
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
|
||||||
|
private final OAuth2User principal;
|
||||||
|
private final ClientRegistration clientRegistration;
|
||||||
|
private final AccessToken accessToken;
|
||||||
|
|
||||||
|
public OAuth2AuthenticationToken(ClientRegistration clientRegistration, AccessToken accessToken) {
|
||||||
|
this(null, AuthorityUtils.NO_AUTHORITIES, clientRegistration, accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OAuth2AuthenticationToken(OAuth2User principal, Collection<? extends GrantedAuthority> authorities,
|
||||||
|
ClientRegistration clientRegistration, AccessToken accessToken) {
|
||||||
|
|
||||||
|
super(authorities);
|
||||||
|
Assert.notNull(clientRegistration, "clientRegistration cannot be null");
|
||||||
|
Assert.notNull(accessToken, "accessToken cannot be null");
|
||||||
|
this.principal = principal;
|
||||||
|
this.clientRegistration = clientRegistration;
|
||||||
|
this.accessToken = accessToken;
|
||||||
|
this.setAuthenticated(principal != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getPrincipal() {
|
||||||
|
return this.principal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getCredentials() {
|
||||||
|
// Credentials are never exposed (by the Provider) for an OAuth2 User
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientRegistration getClientRegistration() {
|
||||||
|
return this.clientRegistration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccessToken getAccessToken() {
|
||||||
|
return this.accessToken;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client.authentication.nimbus;
|
||||||
|
|
||||||
|
|
||||||
|
import com.nimbusds.oauth2.sdk.*;
|
||||||
|
import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
|
||||||
|
import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
|
||||||
|
import com.nimbusds.oauth2.sdk.auth.ClientSecretPost;
|
||||||
|
import com.nimbusds.oauth2.sdk.auth.Secret;
|
||||||
|
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
|
||||||
|
import com.nimbusds.oauth2.sdk.id.ClientID;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.security.authentication.AuthenticationServiceException;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.AuthorizationGrantTokenExchanger;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.core.AccessToken;
|
||||||
|
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.TokenResponseAttributes;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of an {@link AuthorizationGrantTokenExchanger} that <i>"exchanges"</i>
|
||||||
|
* an <i>authorization code</i> credential for an <i>access token</i> credential
|
||||||
|
* at the authorization server's <i>Token Endpoint</i>.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* <b>NOTE:</b> This implementation uses the <b>Nimbus OAuth 2.0 SDK</b> internally.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
* @see AuthorizationCodeAuthenticationToken
|
||||||
|
* @see TokenResponseAttributes
|
||||||
|
* @see <a target="_blank" href="https://connect2id.com/products/nimbus-oauth-openid-connect-sdk">Nimbus OAuth 2.0 SDK</a>
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request (Authorization Code Grant)</a>
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response (Authorization Code Grant)</a>
|
||||||
|
*/
|
||||||
|
public class NimbusAuthorizationCodeTokenExchanger implements AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> {
|
||||||
|
private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TokenResponseAttributes exchange(AuthorizationCodeAuthenticationToken authorizationCodeAuthenticationToken)
|
||||||
|
throws OAuth2AuthenticationException {
|
||||||
|
|
||||||
|
ClientRegistration clientRegistration = authorizationCodeAuthenticationToken.getClientRegistration();
|
||||||
|
|
||||||
|
// Build the authorization code grant request for the token endpoint
|
||||||
|
AuthorizationCode authorizationCode = new AuthorizationCode(authorizationCodeAuthenticationToken.getAuthorizationCode());
|
||||||
|
URI redirectUri = this.toURI(clientRegistration.getRedirectUri());
|
||||||
|
AuthorizationGrant authorizationCodeGrant = new AuthorizationCodeGrant(authorizationCode, redirectUri);
|
||||||
|
URI tokenUri = this.toURI(clientRegistration.getProviderDetails().getTokenUri());
|
||||||
|
|
||||||
|
// Set the credentials to authenticate the client at the token endpoint
|
||||||
|
ClientID clientId = new ClientID(clientRegistration.getClientId());
|
||||||
|
Secret clientSecret = new Secret(clientRegistration.getClientSecret());
|
||||||
|
ClientAuthentication clientAuthentication;
|
||||||
|
if (ClientAuthenticationMethod.FORM.equals(clientRegistration.getClientAuthenticationMethod())) {
|
||||||
|
clientAuthentication = new ClientSecretPost(clientId, clientSecret);
|
||||||
|
} else {
|
||||||
|
clientAuthentication = new ClientSecretBasic(clientId, clientSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
TokenResponse tokenResponse;
|
||||||
|
try {
|
||||||
|
// Send the Access Token request
|
||||||
|
TokenRequest tokenRequest = new TokenRequest(tokenUri, clientAuthentication, authorizationCodeGrant);
|
||||||
|
HTTPRequest httpRequest = tokenRequest.toHTTPRequest();
|
||||||
|
httpRequest.setAccept(MediaType.APPLICATION_JSON_VALUE);
|
||||||
|
tokenResponse = TokenResponse.parse(httpRequest.send());
|
||||||
|
} catch (ParseException pe) {
|
||||||
|
// This error occurs if the Access Token Response is not well-formed,
|
||||||
|
// for example, a required attribute is missing
|
||||||
|
throw new OAuth2AuthenticationException(new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE), pe);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
// This error occurs when there is a network-related issue
|
||||||
|
throw new AuthenticationServiceException("An error occurred while sending the Access Token Request: " +
|
||||||
|
ioe.getMessage(), ioe);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tokenResponse.indicatesSuccess()) {
|
||||||
|
TokenErrorResponse tokenErrorResponse = (TokenErrorResponse) tokenResponse;
|
||||||
|
ErrorObject errorObject = tokenErrorResponse.getErrorObject();
|
||||||
|
OAuth2Error oauth2Error = new OAuth2Error(errorObject.getCode(), errorObject.getDescription(),
|
||||||
|
(errorObject.getURI() != null ? errorObject.getURI().toString() : null));
|
||||||
|
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
AccessTokenResponse accessTokenResponse = (AccessTokenResponse) tokenResponse;
|
||||||
|
|
||||||
|
String accessToken = accessTokenResponse.getTokens().getAccessToken().getValue();
|
||||||
|
AccessToken.TokenType accessTokenType = null;
|
||||||
|
if (AccessToken.TokenType.BEARER.value().equals(accessTokenResponse.getTokens().getAccessToken().getType().getValue())) {
|
||||||
|
accessTokenType = AccessToken.TokenType.BEARER;
|
||||||
|
}
|
||||||
|
long expiresIn = accessTokenResponse.getTokens().getAccessToken().getLifetime();
|
||||||
|
Set<String> scopes = Collections.emptySet();
|
||||||
|
if (!CollectionUtils.isEmpty(accessTokenResponse.getTokens().getAccessToken().getScope())) {
|
||||||
|
scopes = new HashSet<>(accessTokenResponse.getTokens().getAccessToken().getScope().toStringList());
|
||||||
|
}
|
||||||
|
Map<String, Object> additionalParameters = accessTokenResponse.getCustomParameters().entrySet().stream()
|
||||||
|
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||||
|
|
||||||
|
return TokenResponseAttributes.withToken(accessToken)
|
||||||
|
.tokenType(accessTokenType)
|
||||||
|
.expiresIn(expiresIn)
|
||||||
|
.scopes(scopes)
|
||||||
|
.additionalParameters(additionalParameters)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private URI toURI(String uriStr) {
|
||||||
|
try {
|
||||||
|
return new URI(uriStr);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new IllegalArgumentException("An error occurred parsing URI: " + uriStr, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,279 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client.registration;
|
||||||
|
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
*/
|
||||||
|
public class ClientRegistration {
|
||||||
|
private String clientId;
|
||||||
|
private String clientSecret;
|
||||||
|
private ClientAuthenticationMethod clientAuthenticationMethod = ClientAuthenticationMethod.HEADER;
|
||||||
|
private AuthorizationGrantType authorizedGrantType;
|
||||||
|
private String redirectUri;
|
||||||
|
private Set<String> scopes = Collections.emptySet();
|
||||||
|
private ProviderDetails providerDetails = new ProviderDetails();
|
||||||
|
private String clientName;
|
||||||
|
private String clientAlias;
|
||||||
|
|
||||||
|
protected ClientRegistration() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientId() {
|
||||||
|
return this.clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setClientId(String clientId) {
|
||||||
|
this.clientId = clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientSecret() {
|
||||||
|
return this.clientSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setClientSecret(String clientSecret) {
|
||||||
|
this.clientSecret = clientSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientAuthenticationMethod getClientAuthenticationMethod() {
|
||||||
|
return this.clientAuthenticationMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setClientAuthenticationMethod(ClientAuthenticationMethod clientAuthenticationMethod) {
|
||||||
|
this.clientAuthenticationMethod = clientAuthenticationMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuthorizationGrantType getAuthorizedGrantType() {
|
||||||
|
return this.authorizedGrantType;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setAuthorizedGrantType(AuthorizationGrantType authorizedGrantType) {
|
||||||
|
this.authorizedGrantType = authorizedGrantType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRedirectUri() {
|
||||||
|
return this.redirectUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setRedirectUri(String redirectUri) {
|
||||||
|
this.redirectUri = redirectUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> getScopes() {
|
||||||
|
return this.scopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setScopes(Set<String> scopes) {
|
||||||
|
this.scopes = scopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProviderDetails getProviderDetails() {
|
||||||
|
return this.providerDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setProviderDetails(ProviderDetails providerDetails) {
|
||||||
|
this.providerDetails = providerDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientName() {
|
||||||
|
return this.clientName;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setClientName(String clientName) {
|
||||||
|
this.clientName = clientName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientAlias() {
|
||||||
|
return this.clientAlias;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setClientAlias(String clientAlias) {
|
||||||
|
this.clientAlias = clientAlias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ProviderDetails {
|
||||||
|
private String authorizationUri;
|
||||||
|
private String tokenUri;
|
||||||
|
private String userInfoUri;
|
||||||
|
|
||||||
|
protected ProviderDetails() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAuthorizationUri() {
|
||||||
|
return this.authorizationUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setAuthorizationUri(String authorizationUri) {
|
||||||
|
this.authorizationUri = authorizationUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTokenUri() {
|
||||||
|
return this.tokenUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setTokenUri(String tokenUri) {
|
||||||
|
this.tokenUri = tokenUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserInfoUri() {
|
||||||
|
return this.userInfoUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setUserInfoUri(String userInfoUri) {
|
||||||
|
this.userInfoUri = userInfoUri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
protected String clientId;
|
||||||
|
protected String clientSecret;
|
||||||
|
protected ClientAuthenticationMethod clientAuthenticationMethod = ClientAuthenticationMethod.HEADER;
|
||||||
|
protected AuthorizationGrantType authorizedGrantType;
|
||||||
|
protected String redirectUri;
|
||||||
|
protected Set<String> scopes;
|
||||||
|
protected String authorizationUri;
|
||||||
|
protected String tokenUri;
|
||||||
|
protected String userInfoUri;
|
||||||
|
protected String clientName;
|
||||||
|
protected String clientAlias;
|
||||||
|
|
||||||
|
public Builder(String clientId) {
|
||||||
|
this.clientId = clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder(ClientRegistrationProperties clientRegistrationProperties) {
|
||||||
|
this(clientRegistrationProperties.getClientId());
|
||||||
|
this.clientSecret(clientRegistrationProperties.getClientSecret());
|
||||||
|
this.clientAuthenticationMethod(clientRegistrationProperties.getClientAuthenticationMethod());
|
||||||
|
this.authorizedGrantType(clientRegistrationProperties.getAuthorizedGrantType());
|
||||||
|
this.redirectUri(clientRegistrationProperties.getRedirectUri());
|
||||||
|
if (!CollectionUtils.isEmpty(clientRegistrationProperties.getScopes())) {
|
||||||
|
this.scopes(clientRegistrationProperties.getScopes().stream().toArray(String[]::new));
|
||||||
|
}
|
||||||
|
this.authorizationUri(clientRegistrationProperties.getAuthorizationUri());
|
||||||
|
this.tokenUri(clientRegistrationProperties.getTokenUri());
|
||||||
|
this.userInfoUri(clientRegistrationProperties.getUserInfoUri());
|
||||||
|
this.clientName(clientRegistrationProperties.getClientName());
|
||||||
|
this.clientAlias(clientRegistrationProperties.getClientAlias());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder clientSecret(String clientSecret) {
|
||||||
|
this.clientSecret = clientSecret;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder clientAuthenticationMethod(ClientAuthenticationMethod clientAuthenticationMethod) {
|
||||||
|
this.clientAuthenticationMethod = clientAuthenticationMethod;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder authorizedGrantType(AuthorizationGrantType authorizedGrantType) {
|
||||||
|
this.authorizedGrantType = authorizedGrantType;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder redirectUri(String redirectUri) {
|
||||||
|
this.redirectUri = redirectUri;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder scopes(String... scopes) {
|
||||||
|
if (scopes != null && scopes.length > 0) {
|
||||||
|
this.scopes = Collections.unmodifiableSet(
|
||||||
|
new LinkedHashSet<>(Arrays.asList(scopes)));
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder authorizationUri(String authorizationUri) {
|
||||||
|
this.authorizationUri = authorizationUri;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder tokenUri(String tokenUri) {
|
||||||
|
this.tokenUri = tokenUri;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder userInfoUri(String userInfoUri) {
|
||||||
|
this.userInfoUri = userInfoUri;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder clientName(String clientName) {
|
||||||
|
this.clientName = clientName;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder clientAlias(String clientAlias) {
|
||||||
|
this.clientAlias = clientAlias;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientRegistration build() {
|
||||||
|
this.validateClientWithAuthorizationCodeGrantType();
|
||||||
|
ClientRegistration clientRegistration = new ClientRegistration();
|
||||||
|
this.setProperties(clientRegistration);
|
||||||
|
return clientRegistration;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setProperties(ClientRegistration clientRegistration) {
|
||||||
|
clientRegistration.setClientId(this.clientId);
|
||||||
|
clientRegistration.setClientSecret(this.clientSecret);
|
||||||
|
clientRegistration.setClientAuthenticationMethod(this.clientAuthenticationMethod);
|
||||||
|
clientRegistration.setAuthorizedGrantType(this.authorizedGrantType);
|
||||||
|
clientRegistration.setRedirectUri(this.redirectUri);
|
||||||
|
clientRegistration.setScopes(this.scopes);
|
||||||
|
|
||||||
|
ProviderDetails providerDetails = clientRegistration.new ProviderDetails();
|
||||||
|
providerDetails.setAuthorizationUri(this.authorizationUri);
|
||||||
|
providerDetails.setTokenUri(this.tokenUri);
|
||||||
|
providerDetails.setUserInfoUri(this.userInfoUri);
|
||||||
|
clientRegistration.setProviderDetails(providerDetails);
|
||||||
|
|
||||||
|
clientRegistration.setClientName(this.clientName);
|
||||||
|
clientRegistration.setClientAlias(this.clientAlias);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void validateClientWithAuthorizationCodeGrantType() {
|
||||||
|
Assert.isTrue(AuthorizationGrantType.AUTHORIZATION_CODE.equals(this.authorizedGrantType),
|
||||||
|
"authorizedGrantType must be " + AuthorizationGrantType.AUTHORIZATION_CODE.value());
|
||||||
|
Assert.hasText(this.clientId, "clientId cannot be empty");
|
||||||
|
Assert.hasText(this.clientSecret, "clientSecret cannot be empty");
|
||||||
|
Assert.notNull(this.clientAuthenticationMethod, "clientAuthenticationMethod cannot be null");
|
||||||
|
Assert.hasText(this.redirectUri, "redirectUri cannot be empty");
|
||||||
|
Assert.notEmpty(this.scopes, "scopes cannot be empty");
|
||||||
|
Assert.hasText(this.authorizationUri, "authorizationUri cannot be empty");
|
||||||
|
Assert.hasText(this.tokenUri, "tokenUri cannot be empty");
|
||||||
|
Assert.hasText(this.userInfoUri, "userInfoUri cannot be empty");
|
||||||
|
Assert.hasText(this.clientName, "clientName cannot be empty");
|
||||||
|
Assert.hasText(this.clientAlias, "clientAlias cannot be empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,129 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client.registration;
|
||||||
|
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
*/
|
||||||
|
public class ClientRegistrationProperties {
|
||||||
|
private String clientId;
|
||||||
|
private String clientSecret;
|
||||||
|
private ClientAuthenticationMethod clientAuthenticationMethod = ClientAuthenticationMethod.HEADER;
|
||||||
|
private AuthorizationGrantType authorizedGrantType;
|
||||||
|
private String redirectUri;
|
||||||
|
private Set<String> scopes;
|
||||||
|
private String authorizationUri;
|
||||||
|
private String tokenUri;
|
||||||
|
private String userInfoUri;
|
||||||
|
private String clientName;
|
||||||
|
private String clientAlias;
|
||||||
|
|
||||||
|
|
||||||
|
public String getClientId() {
|
||||||
|
return this.clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClientId(String clientId) {
|
||||||
|
this.clientId = clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientSecret() {
|
||||||
|
return this.clientSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClientSecret(String clientSecret) {
|
||||||
|
this.clientSecret = clientSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientAuthenticationMethod getClientAuthenticationMethod() {
|
||||||
|
return this.clientAuthenticationMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClientAuthenticationMethod(ClientAuthenticationMethod clientAuthenticationMethod) {
|
||||||
|
this.clientAuthenticationMethod = clientAuthenticationMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuthorizationGrantType getAuthorizedGrantType() {
|
||||||
|
return this.authorizedGrantType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAuthorizedGrantType(AuthorizationGrantType authorizedGrantType) {
|
||||||
|
this.authorizedGrantType = authorizedGrantType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRedirectUri() {
|
||||||
|
return this.redirectUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRedirectUri(String redirectUri) {
|
||||||
|
this.redirectUri = redirectUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> getScopes() {
|
||||||
|
return this.scopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setScopes(Set<String> scopes) {
|
||||||
|
this.scopes = scopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAuthorizationUri() {
|
||||||
|
return this.authorizationUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAuthorizationUri(String authorizationUri) {
|
||||||
|
this.authorizationUri = authorizationUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTokenUri() {
|
||||||
|
return this.tokenUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTokenUri(String tokenUri) {
|
||||||
|
this.tokenUri = tokenUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserInfoUri() {
|
||||||
|
return this.userInfoUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserInfoUri(String userInfoUri) {
|
||||||
|
this.userInfoUri = userInfoUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientName() {
|
||||||
|
return this.clientName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClientName(String clientName) {
|
||||||
|
this.clientName = clientName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientAlias() {
|
||||||
|
return this.clientAlias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClientAlias(String clientAlias) {
|
||||||
|
this.clientAlias = clientAlias;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client.registration;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
*/
|
||||||
|
public interface ClientRegistrationRepository {
|
||||||
|
|
||||||
|
ClientRegistration getRegistrationByClientId(String clientId);
|
||||||
|
|
||||||
|
ClientRegistration getRegistrationByClientAlias(String clientAlias);
|
||||||
|
|
||||||
|
List<ClientRegistration> getRegistrations();
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client.registration;
|
||||||
|
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
*/
|
||||||
|
public final class InMemoryClientRegistrationRepository implements ClientRegistrationRepository {
|
||||||
|
private final List<ClientRegistration> clientRegistrations;
|
||||||
|
|
||||||
|
public InMemoryClientRegistrationRepository(List<ClientRegistration> clientRegistrations) {
|
||||||
|
Assert.notEmpty(clientRegistrations, "clientRegistrations cannot be empty");
|
||||||
|
this.clientRegistrations = Collections.unmodifiableList(clientRegistrations);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientRegistration getRegistrationByClientId(String clientId) {
|
||||||
|
Optional<ClientRegistration> clientRegistration =
|
||||||
|
this.clientRegistrations.stream()
|
||||||
|
.filter(c -> c.getClientId().equals(clientId))
|
||||||
|
.findFirst();
|
||||||
|
return clientRegistration.isPresent() ? clientRegistration.get() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientRegistration getRegistrationByClientAlias(String clientAlias) {
|
||||||
|
Optional<ClientRegistration> clientRegistration =
|
||||||
|
this.clientRegistrations.stream()
|
||||||
|
.filter(c -> c.getClientAlias().equals(clientAlias))
|
||||||
|
.findFirst();
|
||||||
|
return clientRegistration.isPresent() ? clientRegistration.get() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ClientRegistration> getRegistrations() {
|
||||||
|
return this.clientRegistrations;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client.user;
|
||||||
|
|
||||||
|
import org.springframework.security.core.AuthenticatedPrincipal;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
|
import org.springframework.security.oauth2.oidc.user.UserInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementations of this interface are responsible for obtaining
|
||||||
|
* the end-user's (resource owner) attributes from the <i>UserInfo Endpoint</i>
|
||||||
|
* using the provided {@link OAuth2AuthenticationToken#getAccessToken()}
|
||||||
|
* and returning an {@link AuthenticatedPrincipal} in the form of an {@link OAuth2User}
|
||||||
|
* (for a standard <i>OAuth 2.0 Provider</i>) or {@link UserInfo} (for an <i>OpenID Connect 1.0 Provider</i>).
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
* @see OAuth2AuthenticationToken
|
||||||
|
* @see AuthenticatedPrincipal
|
||||||
|
* @see OAuth2User
|
||||||
|
* @see UserInfo
|
||||||
|
*/
|
||||||
|
public interface OAuth2UserService {
|
||||||
|
|
||||||
|
OAuth2User loadUser(OAuth2AuthenticationToken token) throws OAuth2AuthenticationException;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client.user.converter;
|
||||||
|
|
||||||
|
import org.springframework.core.convert.converter.Converter;
|
||||||
|
import org.springframework.http.client.ClientHttpResponse;
|
||||||
|
import org.springframework.http.converter.HttpMessageConverter;
|
||||||
|
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
*/
|
||||||
|
public abstract class AbstractOAuth2UserConverter<T extends OAuth2User> implements Converter<ClientHttpResponse, T> {
|
||||||
|
private final HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
|
||||||
|
|
||||||
|
protected AbstractOAuth2UserConverter() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final T convert(ClientHttpResponse clientHttpResponse) {
|
||||||
|
Map<String, Object> userAttributes;
|
||||||
|
|
||||||
|
try {
|
||||||
|
userAttributes = (Map<String, Object>) this.jackson2HttpMessageConverter.read(Map.class, clientHttpResponse);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new IllegalArgumentException("An error occurred reading the UserInfo response: " + ex.getMessage(), ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.convert(userAttributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract T convert(Map<String, Object> userAttributes);
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client.user.converter;
|
||||||
|
|
||||||
|
import org.springframework.core.convert.converter.Converter;
|
||||||
|
import org.springframework.http.client.ClientHttpResponse;
|
||||||
|
import org.springframework.http.converter.HttpMessageConverter;
|
||||||
|
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
*/
|
||||||
|
public final class CustomOAuth2UserConverter<T extends OAuth2User> implements Converter<ClientHttpResponse, T> {
|
||||||
|
private final HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
|
||||||
|
private final Class<T> customType;
|
||||||
|
|
||||||
|
public CustomOAuth2UserConverter(Class<T> customType) {
|
||||||
|
this.customType = customType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T convert(ClientHttpResponse clientHttpResponse) {
|
||||||
|
T user;
|
||||||
|
|
||||||
|
try {
|
||||||
|
user = (T) this.jackson2HttpMessageConverter.read(this.customType, clientHttpResponse);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new IllegalArgumentException("An error occurred reading the UserInfo response: " + ex.getMessage(), ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client.user.converter;
|
||||||
|
|
||||||
|
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
*/
|
||||||
|
public final class OAuth2UserConverter extends AbstractOAuth2UserConverter<OAuth2User> {
|
||||||
|
private final String nameAttributeKey;
|
||||||
|
|
||||||
|
public OAuth2UserConverter(String nameAttributeKey) {
|
||||||
|
Assert.hasText(nameAttributeKey, "nameAttributeKey cannot be empty");
|
||||||
|
this.nameAttributeKey = nameAttributeKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected OAuth2User convert(Map<String, Object> userAttributes) {
|
||||||
|
return new DefaultOAuth2User(userAttributes, this.nameAttributeKey);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client.user.converter;
|
||||||
|
|
||||||
|
import org.springframework.security.oauth2.oidc.user.DefaultUserInfo;
|
||||||
|
import org.springframework.security.oauth2.oidc.user.UserInfo;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
*/
|
||||||
|
public final class UserInfoConverter extends AbstractOAuth2UserConverter<UserInfo> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected UserInfo convert(Map<String, Object> userAttributes) {
|
||||||
|
return new DefaultUserInfo(userAttributes);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client.user.nimbus;
|
||||||
|
|
||||||
|
import com.nimbusds.oauth2.sdk.ErrorObject;
|
||||||
|
import com.nimbusds.oauth2.sdk.ParseException;
|
||||||
|
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
|
||||||
|
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
|
||||||
|
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
|
||||||
|
import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse;
|
||||||
|
import com.nimbusds.openid.connect.sdk.UserInfoRequest;
|
||||||
|
import org.springframework.core.convert.converter.Converter;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.client.ClientHttpResponse;
|
||||||
|
import org.springframework.security.authentication.AuthenticationServiceException;
|
||||||
|
import org.springframework.security.core.AuthenticatedPrincipal;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.client.user.OAuth2UserService;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
|
import org.springframework.security.oauth2.oidc.user.UserInfo;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of an {@link OAuth2UserService} that uses the <b>Nimbus OAuth 2.0 SDK</b> internally.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This implementation uses a <code>Map</code> of <code>Converter</code>'s <i>keyed</i> by <code>URI</code>.
|
||||||
|
* The <code>URI</code> represents the <i>UserInfo Endpoint</i> address and the mapped <code>Converter</code>
|
||||||
|
* is capable of converting the <i>UserInfo Response</i> to either an
|
||||||
|
* {@link OAuth2User} (for a standard <i>OAuth 2.0 Provider</i>) or
|
||||||
|
* {@link UserInfo} (for an <i>OpenID Connect 1.0 Provider</i>).
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
* @see OAuth2AuthenticationToken
|
||||||
|
* @see AuthenticatedPrincipal
|
||||||
|
* @see OAuth2User
|
||||||
|
* @see UserInfo
|
||||||
|
* @see Converter
|
||||||
|
* @see <a target="_blank" href="https://connect2id.com/products/nimbus-oauth-openid-connect-sdk">Nimbus OAuth 2.0 SDK</a>
|
||||||
|
*/
|
||||||
|
public class NimbusOAuth2UserService implements OAuth2UserService {
|
||||||
|
private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";
|
||||||
|
private final Map<URI, Converter<ClientHttpResponse, ? extends OAuth2User>> userInfoTypeConverters;
|
||||||
|
|
||||||
|
public NimbusOAuth2UserService(Map<URI, Converter<ClientHttpResponse, ? extends OAuth2User>> userInfoTypeConverters) {
|
||||||
|
Assert.notEmpty(userInfoTypeConverters, "userInfoTypeConverters cannot be empty");
|
||||||
|
this.userInfoTypeConverters = new HashMap<>(userInfoTypeConverters);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OAuth2User loadUser(OAuth2AuthenticationToken token) throws OAuth2AuthenticationException {
|
||||||
|
OAuth2User user;
|
||||||
|
|
||||||
|
try {
|
||||||
|
ClientRegistration clientRegistration = token.getClientRegistration();
|
||||||
|
|
||||||
|
URI userInfoUri;
|
||||||
|
try {
|
||||||
|
userInfoUri = new URI(clientRegistration.getProviderDetails().getUserInfoUri());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new IllegalArgumentException("An error occurred parsing the userInfo URI: " +
|
||||||
|
clientRegistration.getProviderDetails().getUserInfoUri(), ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
Converter<ClientHttpResponse, ? extends OAuth2User> userInfoConverter = this.userInfoTypeConverters.get(userInfoUri);
|
||||||
|
if (userInfoConverter == null) {
|
||||||
|
throw new IllegalArgumentException("There is no available User Info converter for " + userInfoUri.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
BearerAccessToken accessToken = new BearerAccessToken(token.getAccessToken().getTokenValue());
|
||||||
|
|
||||||
|
// Request the User Info
|
||||||
|
UserInfoRequest userInfoRequest = new UserInfoRequest(userInfoUri, accessToken);
|
||||||
|
HTTPRequest httpRequest = userInfoRequest.toHTTPRequest();
|
||||||
|
httpRequest.setAccept(MediaType.APPLICATION_JSON_VALUE);
|
||||||
|
HTTPResponse httpResponse = httpRequest.send();
|
||||||
|
|
||||||
|
if (httpResponse.getStatusCode() != HTTPResponse.SC_OK) {
|
||||||
|
UserInfoErrorResponse userInfoErrorResponse = UserInfoErrorResponse.parse(httpResponse);
|
||||||
|
ErrorObject errorObject = userInfoErrorResponse.getErrorObject();
|
||||||
|
OAuth2Error oauth2Error = new OAuth2Error(errorObject.getCode(), errorObject.getDescription(),
|
||||||
|
(errorObject.getURI() != null ? errorObject.getURI().toString() : null));
|
||||||
|
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
user = userInfoConverter.convert(new NimbusClientHttpResponse(httpResponse));
|
||||||
|
|
||||||
|
} catch (ParseException ex) {
|
||||||
|
// This error occurs if the User Info Response is not well-formed or invalid
|
||||||
|
throw new OAuth2AuthenticationException(new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE), ex);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
// This error occurs when there is a network-related issue
|
||||||
|
throw new AuthenticationServiceException("An error occurred while sending the User Info Request: " +
|
||||||
|
ex.getMessage(), ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client.web.converter;
|
||||||
|
|
||||||
|
import org.springframework.core.convert.converter.Converter;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2Parameter;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.AuthorizationCodeAuthorizationResponseAttributes;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
*/
|
||||||
|
public final class AuthorizationCodeAuthorizationResponseAttributesConverter implements Converter<HttpServletRequest, AuthorizationCodeAuthorizationResponseAttributes> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthorizationCodeAuthorizationResponseAttributes convert(HttpServletRequest request) {
|
||||||
|
AuthorizationCodeAuthorizationResponseAttributes response;
|
||||||
|
|
||||||
|
String code = request.getParameter(OAuth2Parameter.CODE);
|
||||||
|
Assert.hasText(code, OAuth2Parameter.CODE + " attribute is required");
|
||||||
|
|
||||||
|
String state = request.getParameter(OAuth2Parameter.STATE);
|
||||||
|
|
||||||
|
response = new AuthorizationCodeAuthorizationResponseAttributes(code, state);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client.web.converter;
|
||||||
|
|
||||||
|
import org.springframework.core.convert.converter.Converter;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.ErrorResponseAttributes;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2Parameter;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
*/
|
||||||
|
public final class ErrorResponseAttributesConverter implements Converter<HttpServletRequest, ErrorResponseAttributes> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ErrorResponseAttributes convert(HttpServletRequest request) {
|
||||||
|
ErrorResponseAttributes response;
|
||||||
|
|
||||||
|
String errorCode = request.getParameter(OAuth2Parameter.ERROR);
|
||||||
|
if (!StringUtils.hasText(errorCode)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String description = request.getParameter(OAuth2Parameter.ERROR_DESCRIPTION);
|
||||||
|
String uri = request.getParameter(OAuth2Parameter.ERROR_URI);
|
||||||
|
String state = request.getParameter(OAuth2Parameter.STATE);
|
||||||
|
|
||||||
|
response = ErrorResponseAttributes.withErrorCode(errorCode)
|
||||||
|
.description(description)
|
||||||
|
.uri(uri)
|
||||||
|
.state(state)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,254 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.client.authentication;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.springframework.mock.web.MockHttpServletRequest;
|
||||||
|
import org.springframework.mock.web.MockHttpServletResponse;
|
||||||
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
|
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.AuthorizationRequestAttributes;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2Parameter;
|
||||||
|
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||||
|
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||||
|
|
||||||
|
import javax.servlet.FilterChain;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Matchers.any;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
import static org.springframework.security.oauth2.client.authentication.TestUtil.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests {@link AuthorizationCodeAuthenticationProcessingFilter}.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
*/
|
||||||
|
public class AuthorizationCodeAuthenticationProcessingFilterTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void doFilterWhenNotAuthorizationCodeResponseThenContinueChain() throws Exception {
|
||||||
|
ClientRegistration clientRegistration = googleClientRegistration();
|
||||||
|
|
||||||
|
AuthorizationCodeAuthenticationProcessingFilter filter = spy(setupFilter(clientRegistration));
|
||||||
|
|
||||||
|
String requestURI = "/path";
|
||||||
|
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestURI);
|
||||||
|
request.setServletPath(requestURI);
|
||||||
|
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||||
|
FilterChain filterChain = mock(FilterChain.class);
|
||||||
|
|
||||||
|
filter.doFilter(request, response, filterChain);
|
||||||
|
|
||||||
|
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||||
|
verify(filter, never()).attemptAuthentication(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void doFilterWhenAuthorizationCodeErrorResponseThenAuthenticationFailureHandlerIsCalled() throws Exception {
|
||||||
|
ClientRegistration clientRegistration = githubClientRegistration();
|
||||||
|
|
||||||
|
AuthorizationCodeAuthenticationProcessingFilter filter = spy(setupFilter(clientRegistration));
|
||||||
|
AuthenticationFailureHandler failureHandler = mock(AuthenticationFailureHandler.class);
|
||||||
|
filter.setAuthenticationFailureHandler(failureHandler);
|
||||||
|
|
||||||
|
MockHttpServletRequest request = this.setupRequest(clientRegistration);
|
||||||
|
String errorCode = OAuth2Error.INVALID_GRANT_ERROR_CODE;
|
||||||
|
request.addParameter(OAuth2Parameter.ERROR, errorCode);
|
||||||
|
request.addParameter(OAuth2Parameter.STATE, "some state");
|
||||||
|
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||||
|
FilterChain filterChain = mock(FilterChain.class);
|
||||||
|
|
||||||
|
filter.doFilter(request, response, filterChain);
|
||||||
|
|
||||||
|
verify(filter).attemptAuthentication(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||||
|
verify(failureHandler).onAuthenticationFailure(any(HttpServletRequest.class), any(HttpServletResponse.class),
|
||||||
|
any(AuthenticationException.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void doFilterWhenAuthorizationCodeSuccessResponseThenAuthenticationSuccessHandlerIsCalled() throws Exception {
|
||||||
|
TestingAuthenticationToken authentication = new TestingAuthenticationToken("joe", "password", "user", "admin");
|
||||||
|
AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
|
||||||
|
when(authenticationManager.authenticate(any(Authentication.class))).thenReturn(authentication);
|
||||||
|
|
||||||
|
ClientRegistration clientRegistration = githubClientRegistration();
|
||||||
|
|
||||||
|
AuthorizationCodeAuthenticationProcessingFilter filter = spy(setupFilter(authenticationManager, clientRegistration));
|
||||||
|
AuthenticationSuccessHandler successHandler = mock(AuthenticationSuccessHandler.class);
|
||||||
|
filter.setAuthenticationSuccessHandler(successHandler);
|
||||||
|
AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionAuthorizationRequestRepository();
|
||||||
|
filter.setAuthorizationRequestRepository(authorizationRequestRepository);
|
||||||
|
|
||||||
|
MockHttpServletRequest request = this.setupRequest(clientRegistration);
|
||||||
|
String authCode = "some code";
|
||||||
|
String state = "some state";
|
||||||
|
request.addParameter(OAuth2Parameter.CODE, authCode);
|
||||||
|
request.addParameter(OAuth2Parameter.STATE, state);
|
||||||
|
setupAuthorizationRequest(authorizationRequestRepository, request, clientRegistration, state);
|
||||||
|
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||||
|
FilterChain filterChain = mock(FilterChain.class);
|
||||||
|
|
||||||
|
filter.doFilter(request, response, filterChain);
|
||||||
|
|
||||||
|
verify(filter).attemptAuthentication(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||||
|
|
||||||
|
ArgumentCaptor<Authentication> authenticationArgCaptor = ArgumentCaptor.forClass(Authentication.class);
|
||||||
|
verify(successHandler).onAuthenticationSuccess(any(HttpServletRequest.class), any(HttpServletResponse.class),
|
||||||
|
authenticationArgCaptor.capture());
|
||||||
|
assertThat(authenticationArgCaptor.getValue()).isEqualTo(authentication);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void doFilterWhenAuthorizationCodeSuccessResponseAndNoMatchingAuthorizationRequestThenThrowOAuth2AuthenticationExceptionAuthorizationRequestNotFound() throws Exception {
|
||||||
|
ClientRegistration clientRegistration = githubClientRegistration();
|
||||||
|
|
||||||
|
AuthorizationCodeAuthenticationProcessingFilter filter = spy(setupFilter(clientRegistration));
|
||||||
|
AuthenticationFailureHandler failureHandler = mock(AuthenticationFailureHandler.class);
|
||||||
|
filter.setAuthenticationFailureHandler(failureHandler);
|
||||||
|
|
||||||
|
MockHttpServletRequest request = this.setupRequest(clientRegistration);
|
||||||
|
String authCode = "some code";
|
||||||
|
String state = "some state";
|
||||||
|
request.addParameter(OAuth2Parameter.CODE, authCode);
|
||||||
|
request.addParameter(OAuth2Parameter.STATE, state);
|
||||||
|
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||||
|
FilterChain filterChain = mock(FilterChain.class);
|
||||||
|
|
||||||
|
filter.doFilter(request, response, filterChain);
|
||||||
|
|
||||||
|
verifyThrowsOAuth2AuthenticationExceptionWithErrorCode(filter, failureHandler, "authorization_request_not_found");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void doFilterWhenAuthorizationCodeSuccessResponseWithInvalidStateParamThenThrowOAuth2AuthenticationExceptionInvalidStateParameter() throws Exception {
|
||||||
|
ClientRegistration clientRegistration = githubClientRegistration();
|
||||||
|
|
||||||
|
AuthorizationCodeAuthenticationProcessingFilter filter = spy(setupFilter(clientRegistration));
|
||||||
|
AuthenticationFailureHandler failureHandler = mock(AuthenticationFailureHandler.class);
|
||||||
|
filter.setAuthenticationFailureHandler(failureHandler);
|
||||||
|
AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionAuthorizationRequestRepository();
|
||||||
|
filter.setAuthorizationRequestRepository(authorizationRequestRepository);
|
||||||
|
|
||||||
|
MockHttpServletRequest request = this.setupRequest(clientRegistration);
|
||||||
|
String authCode = "some code";
|
||||||
|
String state = "some other state";
|
||||||
|
request.addParameter(OAuth2Parameter.CODE, authCode);
|
||||||
|
request.addParameter(OAuth2Parameter.STATE, state);
|
||||||
|
setupAuthorizationRequest(authorizationRequestRepository, request, clientRegistration, "some state");
|
||||||
|
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||||
|
FilterChain filterChain = mock(FilterChain.class);
|
||||||
|
|
||||||
|
filter.doFilter(request, response, filterChain);
|
||||||
|
|
||||||
|
verifyThrowsOAuth2AuthenticationExceptionWithErrorCode(filter, failureHandler, "invalid_state_parameter");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void doFilterWhenAuthorizationCodeSuccessResponseWithInvalidRedirectUriParamThenThrowOAuth2AuthenticationExceptionInvalidRedirectUriParameter() throws Exception {
|
||||||
|
ClientRegistration clientRegistration = githubClientRegistration();
|
||||||
|
|
||||||
|
AuthorizationCodeAuthenticationProcessingFilter filter = spy(setupFilter(clientRegistration));
|
||||||
|
AuthenticationFailureHandler failureHandler = mock(AuthenticationFailureHandler.class);
|
||||||
|
filter.setAuthenticationFailureHandler(failureHandler);
|
||||||
|
AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionAuthorizationRequestRepository();
|
||||||
|
filter.setAuthorizationRequestRepository(authorizationRequestRepository);
|
||||||
|
|
||||||
|
MockHttpServletRequest request = this.setupRequest(clientRegistration);
|
||||||
|
request.setRequestURI(request.getRequestURI() + "-other");
|
||||||
|
String authCode = "some code";
|
||||||
|
String state = "some state";
|
||||||
|
request.addParameter(OAuth2Parameter.CODE, authCode);
|
||||||
|
request.addParameter(OAuth2Parameter.STATE, state);
|
||||||
|
setupAuthorizationRequest(authorizationRequestRepository, request, clientRegistration, state);
|
||||||
|
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||||
|
FilterChain filterChain = mock(FilterChain.class);
|
||||||
|
|
||||||
|
filter.doFilter(request, response, filterChain);
|
||||||
|
|
||||||
|
verifyThrowsOAuth2AuthenticationExceptionWithErrorCode(filter, failureHandler, "invalid_redirect_uri_parameter");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyThrowsOAuth2AuthenticationExceptionWithErrorCode(AuthorizationCodeAuthenticationProcessingFilter filter,
|
||||||
|
AuthenticationFailureHandler failureHandler,
|
||||||
|
String errorCode) throws Exception {
|
||||||
|
|
||||||
|
verify(filter).attemptAuthentication(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||||
|
|
||||||
|
ArgumentCaptor<AuthenticationException> authenticationExceptionArgCaptor =
|
||||||
|
ArgumentCaptor.forClass(AuthenticationException.class);
|
||||||
|
verify(failureHandler).onAuthenticationFailure(any(HttpServletRequest.class), any(HttpServletResponse.class),
|
||||||
|
authenticationExceptionArgCaptor.capture());
|
||||||
|
assertThat(authenticationExceptionArgCaptor.getValue()).isInstanceOf(OAuth2AuthenticationException.class);
|
||||||
|
OAuth2AuthenticationException oauth2AuthenticationException =
|
||||||
|
(OAuth2AuthenticationException)authenticationExceptionArgCaptor.getValue();
|
||||||
|
assertThat(oauth2AuthenticationException.getErrorObject()).isNotNull();
|
||||||
|
assertThat(oauth2AuthenticationException.getErrorObject().getErrorCode()).isEqualTo(errorCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AuthorizationCodeAuthenticationProcessingFilter setupFilter(ClientRegistration... clientRegistrations) throws Exception {
|
||||||
|
AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
|
||||||
|
|
||||||
|
return setupFilter(authenticationManager, clientRegistrations);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AuthorizationCodeAuthenticationProcessingFilter setupFilter(
|
||||||
|
AuthenticationManager authenticationManager, ClientRegistration... clientRegistrations) throws Exception {
|
||||||
|
|
||||||
|
ClientRegistrationRepository clientRegistrationRepository = clientRegistrationRepository(clientRegistrations);
|
||||||
|
|
||||||
|
AuthorizationCodeAuthenticationProcessingFilter filter = new AuthorizationCodeAuthenticationProcessingFilter();
|
||||||
|
filter.setClientRegistrationRepository(clientRegistrationRepository);
|
||||||
|
filter.setAuthenticationManager(authenticationManager);
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupAuthorizationRequest(AuthorizationRequestRepository authorizationRequestRepository,
|
||||||
|
HttpServletRequest request,
|
||||||
|
ClientRegistration clientRegistration,
|
||||||
|
String state) {
|
||||||
|
|
||||||
|
AuthorizationRequestAttributes authorizationRequestAttributes =
|
||||||
|
AuthorizationRequestAttributes.withAuthorizationCode()
|
||||||
|
.clientId(clientRegistration.getClientId())
|
||||||
|
.authorizeUri(clientRegistration.getProviderDetails().getAuthorizationUri())
|
||||||
|
.redirectUri(clientRegistration.getRedirectUri())
|
||||||
|
.scopes(clientRegistration.getScopes())
|
||||||
|
.state(state)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
authorizationRequestRepository.saveAuthorizationRequest(authorizationRequestAttributes, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MockHttpServletRequest setupRequest(ClientRegistration clientRegistration) {
|
||||||
|
String requestURI = AUTHORIZE_BASE_URI + "/" + clientRegistration.getClientAlias();
|
||||||
|
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestURI);
|
||||||
|
request.setScheme(DEFAULT_SCHEME);
|
||||||
|
request.setServerName(DEFAULT_SERVER_NAME);
|
||||||
|
request.setServerPort(DEFAULT_SERVER_PORT);
|
||||||
|
request.setServletPath(requestURI);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-oauth2-core</artifactId>
|
||||||
|
<version>5.0.0.BUILD-SNAPSHOT</version>
|
||||||
|
<name>spring-security-oauth2-core</name>
|
||||||
|
<description>spring-security-oauth2-core</description>
|
||||||
|
<url>http://spring.io/spring-security</url>
|
||||||
|
<organization>
|
||||||
|
<name>spring.io</name>
|
||||||
|
<url>http://spring.io/</url>
|
||||||
|
</organization>
|
||||||
|
<licenses>
|
||||||
|
<license>
|
||||||
|
<name>The Apache Software License, Version 2.0</name>
|
||||||
|
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
|
||||||
|
<distribution>repo</distribution>
|
||||||
|
</license>
|
||||||
|
</licenses>
|
||||||
|
<developers>
|
||||||
|
<developer>
|
||||||
|
<id>rwinch</id>
|
||||||
|
<name>Rob Winch</name>
|
||||||
|
<email>rwinch@pivotal.io</email>
|
||||||
|
</developer>
|
||||||
|
<developer>
|
||||||
|
<id>jgrandja</id>
|
||||||
|
<name>Joe Grandja</name>
|
||||||
|
<email>jgrandja@pivotal.io</email>
|
||||||
|
</developer>
|
||||||
|
</developers>
|
||||||
|
<scm>
|
||||||
|
<connection>scm:git:git://github.com/spring-projects/spring-security</connection>
|
||||||
|
<developerConnection>scm:git:git://github.com/spring-projects/spring-security</developerConnection>
|
||||||
|
<url>https://github.com/spring-projects/spring-security</url>
|
||||||
|
</scm>
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework</groupId>
|
||||||
|
<artifactId>spring-framework-bom</artifactId>
|
||||||
|
<version>4.3.5.RELEASE</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-core</artifactId>
|
||||||
|
<version>5.0.0.BUILD-SNAPSHOT</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework</groupId>
|
||||||
|
<artifactId>spring-core</artifactId>
|
||||||
|
<scope>compile</scope>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<artifactId>commons-logging</artifactId>
|
||||||
|
<groupId>commons-logging</groupId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework</groupId>
|
||||||
|
<artifactId>spring-web</artifactId>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>commons-logging</groupId>
|
||||||
|
<artifactId>commons-logging</artifactId>
|
||||||
|
<version>1.2</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>javax.servlet</groupId>
|
||||||
|
<artifactId>javax.servlet-api</artifactId>
|
||||||
|
<version>3.1.0</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>ch.qos.logback</groupId>
|
||||||
|
<artifactId>logback-classic</artifactId>
|
||||||
|
<version>1.1.2</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<version>4.12</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.assertj</groupId>
|
||||||
|
<artifactId>assertj-core</artifactId>
|
||||||
|
<version>3.6.2</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-core</artifactId>
|
||||||
|
<version>1.10.19</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>jcl-over-slf4j</artifactId>
|
||||||
|
<version>1.7.7</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework</groupId>
|
||||||
|
<artifactId>spring-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>spring-snapshot</id>
|
||||||
|
<url>https://repo.spring.io/snapshot</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<source>1.8</source>
|
||||||
|
<target>1.8</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
|
@ -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'
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.core;
|
||||||
|
|
||||||
|
import org.springframework.security.core.SpringSecurityCoreVersion;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for <i>Security Token</i> implementations.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* It is highly recommended that implementations be immutable.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
*/
|
||||||
|
public abstract class AbstractToken implements Serializable {
|
||||||
|
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
|
||||||
|
private final String tokenValue;
|
||||||
|
private final Instant issuedAt;
|
||||||
|
private final Instant expiresAt;
|
||||||
|
|
||||||
|
protected AbstractToken(String tokenValue, Instant issuedAt, Instant expiresAt) {
|
||||||
|
Assert.hasLength(tokenValue, "tokenValue cannot be empty");
|
||||||
|
Assert.notNull(issuedAt, "issuedAt cannot be null");
|
||||||
|
Assert.notNull(expiresAt, "expiresAt cannot be null");
|
||||||
|
this.tokenValue = tokenValue;
|
||||||
|
this.issuedAt = issuedAt;
|
||||||
|
this.expiresAt = expiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTokenValue() {
|
||||||
|
return this.tokenValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Instant getIssuedAt() {
|
||||||
|
return this.issuedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Instant getExpiresAt() {
|
||||||
|
return this.expiresAt;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.core;
|
||||||
|
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of an {@link AbstractToken} representing an <i>OAuth 2.0 Access Token</i>.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* An access token is a credential that represents an authorization
|
||||||
|
* granted by the resource owner to the client.
|
||||||
|
* It is primarily used by the client to access protected resources on either a
|
||||||
|
* resource server or the authorization server that originally issued the access token.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.4">Section 1.4 Access Token</a>
|
||||||
|
*/
|
||||||
|
public class AccessToken extends AbstractToken {
|
||||||
|
private final TokenType tokenType;
|
||||||
|
private final Set<String> scopes;
|
||||||
|
private final Map<String,Object> additionalParameters;
|
||||||
|
|
||||||
|
public enum TokenType {
|
||||||
|
BEARER("Bearer");
|
||||||
|
|
||||||
|
private final String value;
|
||||||
|
|
||||||
|
TokenType(String value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String value() {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccessToken(TokenType tokenType, String tokenValue, Instant issuedAt, Instant expiresAt) {
|
||||||
|
this(tokenType, tokenValue, issuedAt, expiresAt, Collections.emptySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccessToken(TokenType tokenType, String tokenValue, Instant issuedAt, Instant expiresAt, Set<String> scopes) {
|
||||||
|
this(tokenType, tokenValue, issuedAt, expiresAt, scopes, Collections.emptyMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccessToken(TokenType tokenType, String tokenValue, Instant issuedAt, Instant expiresAt,
|
||||||
|
Set<String> scopes, Map<String,Object> additionalParameters) {
|
||||||
|
|
||||||
|
super(tokenValue, issuedAt, expiresAt);
|
||||||
|
Assert.notNull(tokenType, "tokenType cannot be null");
|
||||||
|
this.tokenType = tokenType;
|
||||||
|
this.scopes = Collections.unmodifiableSet(
|
||||||
|
scopes != null ? scopes : Collections.emptySet());
|
||||||
|
this.additionalParameters = Collections.unmodifiableMap(
|
||||||
|
additionalParameters != null ? additionalParameters : Collections.emptyMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
public TokenType getTokenType() {
|
||||||
|
return this.tokenType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> getScopes() {
|
||||||
|
return this.scopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getAdditionalParameters() {
|
||||||
|
return additionalParameters;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.core;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An authorization grant is a credential representing the resource owner's authorization
|
||||||
|
* (to access it's protected resources) to the client and used by the client to obtain an access token.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The <i>OAuth 2.0 Authorization Framework</i> defines four standard grant types:
|
||||||
|
* authorization code, implicit, resource owner password credentials, and client credentials.
|
||||||
|
* It also provides an extensibility mechanism for defining additional grant types.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* <b>NOTE:</b> "authorization code" is currently the only supported grant type.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3">Section 1.3 Authorization Grant</a>
|
||||||
|
*/
|
||||||
|
public enum AuthorizationGrantType {
|
||||||
|
AUTHORIZATION_CODE("authorization_code");
|
||||||
|
|
||||||
|
private final String value;
|
||||||
|
|
||||||
|
AuthorizationGrantType(String value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String value() {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.core;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The available authentication methods used when authenticating the client with the authorization server.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-2.3">Section 2.3 Client Authentication</a>
|
||||||
|
*/
|
||||||
|
public enum ClientAuthenticationMethod {
|
||||||
|
HEADER("header"),
|
||||||
|
FORM("form");
|
||||||
|
|
||||||
|
private final String value;
|
||||||
|
|
||||||
|
ClientAuthenticationMethod(String value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String value() {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.core;
|
||||||
|
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A representation of an <i>OAuth 2.0 Error</i>.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* At a minimum, an error response will contain an error code.
|
||||||
|
* The error code may be one of the standard codes defined by the specification,
|
||||||
|
* or a <i>new</i> code defined in the <i>OAuth Extensions Error Registry</i>,
|
||||||
|
* for cases where protocol extensions require additional error code(s) above the standard codes.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-11.4">Section 11.4 OAuth Extensions Error Registry</a>
|
||||||
|
*/
|
||||||
|
public class OAuth2Error {
|
||||||
|
// Standard error codes
|
||||||
|
public static final String INVALID_REQUEST_ERROR_CODE = "invalid_request";
|
||||||
|
public static final String INVALID_CLIENT_ERROR_CODE = "invalid_client";
|
||||||
|
public static final String INVALID_GRANT_ERROR_CODE = "invalid_grant";
|
||||||
|
public static final String UNAUTHORIZED_CLIENT_ERROR_CODE = "unauthorized_client";
|
||||||
|
public static final String UNSUPPORTED_GRANT_TYPE_ERROR_CODE = "unsupported_grant_type";
|
||||||
|
public static final String INVALID_SCOPE_ERROR_CODE = "invalid_scope";
|
||||||
|
|
||||||
|
private final String errorCode;
|
||||||
|
private final String description;
|
||||||
|
private final String uri;
|
||||||
|
|
||||||
|
public OAuth2Error(String errorCode) {
|
||||||
|
this(errorCode, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OAuth2Error(String errorCode, String description, String uri) {
|
||||||
|
Assert.hasText(errorCode, "errorCode cannot be empty");
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
this.description = description;
|
||||||
|
this.uri = uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getErrorCode() {
|
||||||
|
return this.errorCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return this.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUri() {
|
||||||
|
return this.uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "[" + this.getErrorCode() + "] " +
|
||||||
|
(this.getDescription() != null ? this.getDescription() : "");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.core.endpoint;
|
||||||
|
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A representation of an <i>OAuth 2.0 Authorization Response</i> for the authorization code grant type.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.2">Section 4.1.2 Authorization Response</a>
|
||||||
|
*/
|
||||||
|
public final class AuthorizationCodeAuthorizationResponseAttributes {
|
||||||
|
private final String code;
|
||||||
|
private final String state;
|
||||||
|
|
||||||
|
public AuthorizationCodeAuthorizationResponseAttributes(String code, String state) {
|
||||||
|
Assert.notNull(code, "code cannot be null");
|
||||||
|
this.code = code;
|
||||||
|
this.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCode() {
|
||||||
|
return this.code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getState() {
|
||||||
|
return this.state;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.core.endpoint;
|
||||||
|
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A representation of an <i>OAuth 2.0 Access Token Request</i> for the authorization code grant type.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request</a>
|
||||||
|
*/
|
||||||
|
public final class AuthorizationCodeTokenRequestAttributes {
|
||||||
|
private String code;
|
||||||
|
private String clientId;
|
||||||
|
private String redirectUri;
|
||||||
|
|
||||||
|
private AuthorizationCodeTokenRequestAttributes() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCode() {
|
||||||
|
return this.code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientId() {
|
||||||
|
return this.clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRedirectUri() {
|
||||||
|
return this.redirectUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder withCode(String code) {
|
||||||
|
return new Builder(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private final AuthorizationCodeTokenRequestAttributes authorizationCodeTokenRequest;
|
||||||
|
|
||||||
|
private Builder(String code) {
|
||||||
|
Assert.hasText(code, "code cannot be empty");
|
||||||
|
this.authorizationCodeTokenRequest = new AuthorizationCodeTokenRequestAttributes();
|
||||||
|
this.authorizationCodeTokenRequest.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder clientId(String clientId) {
|
||||||
|
Assert.hasText(clientId, "clientId cannot be empty");
|
||||||
|
this.authorizationCodeTokenRequest.clientId = clientId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder redirectUri(String redirectUri) {
|
||||||
|
Assert.hasText(redirectUri, "redirectUri cannot be empty");
|
||||||
|
this.authorizationCodeTokenRequest.redirectUri = redirectUri;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuthorizationCodeTokenRequestAttributes build() {
|
||||||
|
return this.authorizationCodeTokenRequest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.core.endpoint;
|
||||||
|
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A representation of an <i>OAuth 2.0 Authorization Request</i>
|
||||||
|
* for the authorization code grant type or implicit grant type.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
* @see AuthorizationGrantType
|
||||||
|
* @see ResponseType
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.1">Section 4.1.1 Authorization Code Grant Request</a>
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.2.1">Section 4.2.1 Implicit Grant Request</a>
|
||||||
|
*/
|
||||||
|
public final class AuthorizationRequestAttributes implements Serializable {
|
||||||
|
private String authorizeUri;
|
||||||
|
private AuthorizationGrantType authorizationGrantType;
|
||||||
|
private ResponseType responseType;
|
||||||
|
private String clientId;
|
||||||
|
private String redirectUri;
|
||||||
|
private Set<String> scopes;
|
||||||
|
private String state;
|
||||||
|
|
||||||
|
private AuthorizationRequestAttributes() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAuthorizeUri() {
|
||||||
|
return this.authorizeUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuthorizationGrantType getGrantType() {
|
||||||
|
return this.authorizationGrantType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseType getResponseType() {
|
||||||
|
return this.responseType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientId() {
|
||||||
|
return this.clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRedirectUri() {
|
||||||
|
return this.redirectUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> getScopes() {
|
||||||
|
return this.scopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getState() {
|
||||||
|
return this.state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder withAuthorizationCode() {
|
||||||
|
return new Builder(AuthorizationGrantType.AUTHORIZATION_CODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private final AuthorizationRequestAttributes authorizationRequest;
|
||||||
|
|
||||||
|
private Builder(AuthorizationGrantType authorizationGrantType) {
|
||||||
|
Assert.notNull(authorizationGrantType, "authorizationGrantType cannot be null");
|
||||||
|
this.authorizationRequest = new AuthorizationRequestAttributes();
|
||||||
|
this.authorizationRequest.authorizationGrantType = authorizationGrantType;
|
||||||
|
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationGrantType)) {
|
||||||
|
this.authorizationRequest.responseType = ResponseType.CODE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder authorizeUri(String authorizeUri) {
|
||||||
|
Assert.hasText(authorizeUri, "authorizeUri cannot be empty");
|
||||||
|
this.authorizationRequest.authorizeUri = authorizeUri;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder clientId(String clientId) {
|
||||||
|
Assert.hasText(clientId, "clientId cannot be empty");
|
||||||
|
this.authorizationRequest.clientId = clientId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder redirectUri(String redirectUri) {
|
||||||
|
Assert.hasText(redirectUri, "redirectUri cannot be empty");
|
||||||
|
this.authorizationRequest.redirectUri = redirectUri;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder scopes(Set<String> scopes) {
|
||||||
|
this.authorizationRequest.scopes = Collections.unmodifiableSet(
|
||||||
|
CollectionUtils.isEmpty(scopes) ? Collections.emptySet() : new LinkedHashSet<>(scopes));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder state(String state) {
|
||||||
|
this.authorizationRequest.state = state;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuthorizationRequestAttributes build() {
|
||||||
|
return this.authorizationRequest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.core.endpoint;
|
||||||
|
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A representation of an <i>OAuth 2.0 Error Response</i>.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* An error response may be returned from either of the following locations:
|
||||||
|
* <ul>
|
||||||
|
* <li><a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.2.1">Section 4.1.2.1</a> Authorization Code Grant Response</li>
|
||||||
|
* <li><a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.2.2.1">Section 4.2.2.1</a> Implicit Grant Response</li>
|
||||||
|
* <li><a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-5.2">Section 5.2</a> Access Token Response</li>
|
||||||
|
* <li><a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-7.2">Section 7.2</a> Protected Resource Response</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
*/
|
||||||
|
public final class ErrorResponseAttributes {
|
||||||
|
private OAuth2Error errorObject;
|
||||||
|
private String state;
|
||||||
|
|
||||||
|
private ErrorResponseAttributes() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getErrorCode() {
|
||||||
|
return this.errorObject.getErrorCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return this.errorObject.getDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUri() {
|
||||||
|
return this.errorObject.getUri();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getState() {
|
||||||
|
return this.state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder withErrorCode(String errorCode) {
|
||||||
|
return new Builder(errorCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private String errorCode;
|
||||||
|
private String description;
|
||||||
|
private String uri;
|
||||||
|
private String state;
|
||||||
|
|
||||||
|
private Builder(String errorCode) {
|
||||||
|
Assert.hasText(errorCode, "errorCode cannot be empty");
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder description(String description) {
|
||||||
|
this.description = description;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder uri(String uri) {
|
||||||
|
this.uri = uri;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder state(String state) {
|
||||||
|
this.state = state;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ErrorResponseAttributes build() {
|
||||||
|
ErrorResponseAttributes errorResponse = new ErrorResponseAttributes();
|
||||||
|
errorResponse.errorObject = new OAuth2Error(this.errorCode, this.description, this.uri);
|
||||||
|
errorResponse.state = this.state;
|
||||||
|
return errorResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.core.endpoint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard parameters defined in the OAuth Parameters Registry
|
||||||
|
* and used by the authorization endpoint and token endpoint.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-11.2">11.2 OAuth Parameters Registry</a>
|
||||||
|
*/
|
||||||
|
public interface OAuth2Parameter {
|
||||||
|
|
||||||
|
String RESPONSE_TYPE = "response_type";
|
||||||
|
|
||||||
|
String CLIENT_ID = "client_id";
|
||||||
|
|
||||||
|
String REDIRECT_URI = "redirect_uri";
|
||||||
|
|
||||||
|
String SCOPE = "scope";
|
||||||
|
|
||||||
|
String STATE = "state";
|
||||||
|
|
||||||
|
String CODE = "code";
|
||||||
|
|
||||||
|
String ERROR = "error";
|
||||||
|
|
||||||
|
String ERROR_DESCRIPTION = "error_description";
|
||||||
|
|
||||||
|
String ERROR_URI = "error_uri";
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.core.endpoint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <i>response_type</i> parameter is consumed by the authorization endpoint which
|
||||||
|
* is used by the authorization code grant type and implicit grant type flows.
|
||||||
|
* The client sets the <i>response_type</i> parameter with the desired grant type before initiating the authorization request.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The <i>response_type</i> parameter value may be one of "code" for requesting an authorization code or
|
||||||
|
* "token" for requesting an access token (implicit grant).
|
||||||
|
|
||||||
|
* <p>
|
||||||
|
* <b>NOTE:</b> "code" is currently the only supported response type.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-3.1.1">Section 3.1.1 Response Type</a>
|
||||||
|
*/
|
||||||
|
public enum ResponseType {
|
||||||
|
CODE("code");
|
||||||
|
|
||||||
|
private final String value;
|
||||||
|
|
||||||
|
ResponseType(String value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String value() {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.core.endpoint;
|
||||||
|
|
||||||
|
import org.springframework.security.oauth2.core.AccessToken;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A representation of an <i>OAuth 2.0 Access Token Response</i>.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
* @see AccessToken
|
||||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-5.1">Section 5.1 Access Token Response</a>
|
||||||
|
*/
|
||||||
|
public final class TokenResponseAttributes {
|
||||||
|
private AccessToken accessToken;
|
||||||
|
|
||||||
|
private TokenResponseAttributes() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTokenValue() {
|
||||||
|
return this.accessToken.getTokenValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccessToken.TokenType getTokenType() {
|
||||||
|
return this.accessToken.getTokenType();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Instant getIssuedAt() {
|
||||||
|
return this.accessToken.getIssuedAt();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Instant getExpiresAt() {
|
||||||
|
return this.accessToken.getExpiresAt();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> getScopes() {
|
||||||
|
return this.accessToken.getScopes();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getAdditionalParameters() {
|
||||||
|
return this.accessToken.getAdditionalParameters();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder withToken(String tokenValue) {
|
||||||
|
return new Builder(tokenValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private String tokenValue;
|
||||||
|
private AccessToken.TokenType tokenType;
|
||||||
|
private long expiresIn;
|
||||||
|
private Set<String> scopes;
|
||||||
|
private Map<String,Object> additionalParameters;
|
||||||
|
|
||||||
|
private Builder(String tokenValue) {
|
||||||
|
this.tokenValue = tokenValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder tokenType(AccessToken.TokenType tokenType) {
|
||||||
|
this.tokenType = tokenType;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder expiresIn(long expiresIn) {
|
||||||
|
this.expiresIn = expiresIn;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder scopes(Set<String> scopes) {
|
||||||
|
this.scopes = scopes;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder additionalParameters(Map<String,Object> additionalParameters) {
|
||||||
|
this.additionalParameters = additionalParameters;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TokenResponseAttributes build() {
|
||||||
|
Assert.isTrue(this.expiresIn >= 0, "expiresIn must be a positive number");
|
||||||
|
Instant issuedAt = Instant.now();
|
||||||
|
AccessToken accessToken = new AccessToken(this.tokenType, this.tokenValue, issuedAt,
|
||||||
|
issuedAt.plusSeconds(this.expiresIn), this.scopes, this.additionalParameters);
|
||||||
|
|
||||||
|
TokenResponseAttributes tokenResponse = new TokenResponseAttributes();
|
||||||
|
tokenResponse.accessToken = accessToken;
|
||||||
|
return tokenResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Support classes that model the request/response messages from the
|
||||||
|
* <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-3.1">Authorization Endpoint</a>
|
||||||
|
* and <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-3.2">Token Endpoint</a>.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.core.endpoint;
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Core classes and interfaces providing support for the <i>OAuth 2.0 Authorization Framework</i>.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.core;
|
|
@ -0,0 +1,153 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.core.user;
|
||||||
|
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.SpringSecurityCoreVersion;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default implementation of an {@link OAuth2User}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* User attribute names are <b><i>not</i></b> standardized between providers
|
||||||
|
* and therefore it is required that the user supply the <i>key</i>
|
||||||
|
* for the user's "name" attribute to one of the constructors.
|
||||||
|
* The <i>key</i> will be used for accessing the "name" of the
|
||||||
|
* <code>Principal</code> (user) via {@link #getAttributes()}
|
||||||
|
* and returning it from {@link #getName()}.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
* @see OAuth2User
|
||||||
|
*/
|
||||||
|
public class DefaultOAuth2User implements OAuth2User {
|
||||||
|
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
|
||||||
|
private final Set<GrantedAuthority> authorities;
|
||||||
|
private final Map<String, Object> attributes;
|
||||||
|
private final String nameAttributeKey;
|
||||||
|
|
||||||
|
public DefaultOAuth2User(Map<String, Object> attributes, String nameAttributeKey) {
|
||||||
|
this(Collections.emptySet(), attributes, nameAttributeKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DefaultOAuth2User(Set<GrantedAuthority> authorities, Map<String, Object> attributes, String nameAttributeKey) {
|
||||||
|
Assert.notNull(authorities, "authorities cannot be null");
|
||||||
|
Assert.notEmpty(attributes, "attributes cannot be empty");
|
||||||
|
Assert.hasText(nameAttributeKey, "nameAttributeKey cannot be empty");
|
||||||
|
if (!attributes.containsKey(nameAttributeKey)) {
|
||||||
|
throw new IllegalArgumentException("Invalid nameAttributeKey: " + nameAttributeKey);
|
||||||
|
}
|
||||||
|
this.authorities = Collections.unmodifiableSet(this.sortAuthorities(authorities));
|
||||||
|
this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(attributes));
|
||||||
|
this.nameAttributeKey = nameAttributeKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return this.getAttributes().get(this.nameAttributeKey).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||||
|
return this.authorities;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getAttributes() {
|
||||||
|
return this.attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getAttributeAsString(String key) {
|
||||||
|
Object value = this.getAttributes().get(key);
|
||||||
|
return (value != null ? value.toString() : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Boolean getAttributeAsBoolean(String key) {
|
||||||
|
String value = this.getAttributeAsString(key);
|
||||||
|
return (value != null ? Boolean.valueOf(value) : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Instant getAttributeAsInstant(String key) {
|
||||||
|
String value = this.getAttributeAsString(key);
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Instant.ofEpochSecond(Long.valueOf(value));
|
||||||
|
} catch (NumberFormatException ex) {
|
||||||
|
throw new IllegalArgumentException("Invalid long value: " + ex.getMessage(), ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<GrantedAuthority> sortAuthorities(Set<GrantedAuthority> authorities) {
|
||||||
|
if (CollectionUtils.isEmpty(authorities)) {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
SortedSet<GrantedAuthority> sortedAuthorities =
|
||||||
|
new TreeSet<>((g1, g2) -> g1.getAuthority().compareTo(g2.getAuthority()));
|
||||||
|
authorities.stream().forEach(sortedAuthorities::add);
|
||||||
|
|
||||||
|
return sortedAuthorities;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null || this.getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DefaultOAuth2User that = (DefaultOAuth2User) obj;
|
||||||
|
|
||||||
|
if (!this.getName().equals(that.getName())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!this.getAuthorities().equals(that.getAuthorities())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.getAttributes().equals(that.getAttributes());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result = this.getName().hashCode();
|
||||||
|
result = 31 * result + this.getAuthorities().hashCode();
|
||||||
|
result = 31 * result + this.getAttributes().hashCode();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("Name: [");
|
||||||
|
sb.append(this.getName());
|
||||||
|
sb.append("], Granted Authorities: [");
|
||||||
|
sb.append(this.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining(", ")));
|
||||||
|
sb.append("], User Attributes: [");
|
||||||
|
sb.append(this.getAttributes().entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining(", ")));
|
||||||
|
sb.append("]");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.core.user;
|
||||||
|
|
||||||
|
import org.springframework.security.core.AuthenticatedPrincipal;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A representation of a user <code>Principal</code>
|
||||||
|
* that is registered with a standard <i>OAuth 2.0 Provider</i>.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* An OAuth 2.0 user is composed of one or more attributes, for example,
|
||||||
|
* first name, middle name, last name, email, phone number, address, etc.
|
||||||
|
* Each user attribute has a "name" and "value" and
|
||||||
|
* is keyed by the "name" in {@link #getAttributes()}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* <b>NOTE:</b> Attribute names are <b><i>not</i></b> standardized between providers and therefore will vary.
|
||||||
|
* Please consult the provider's API documentation for the set of supported user attribute names.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Implementation instances of this interface represent an {@link AuthenticatedPrincipal}
|
||||||
|
* which is associated to an {@link Authentication} object
|
||||||
|
* and may be accessed via {@link Authentication#getPrincipal()}.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
* @see DefaultOAuth2User
|
||||||
|
* @see AuthenticatedPrincipal
|
||||||
|
*/
|
||||||
|
public interface OAuth2User extends AuthenticatedPrincipal, Serializable {
|
||||||
|
|
||||||
|
Collection<? extends GrantedAuthority> getAuthorities();
|
||||||
|
|
||||||
|
Map<String, Object> getAttributes();
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Provides a model for an <i>OAuth 2.0</i> representation of a user <code>Principal</code>.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.core.user;
|
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.oidc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Standard Claims defined by the <i>OpenID Connect Core 1.0</i> specification
|
||||||
|
* and returned in either the <i>UserInfo Response</i> or in the <i>ID Token</i>.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims">Standard Claims</a>
|
||||||
|
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse">UserInfo Response</a>
|
||||||
|
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#IDToken">ID Token</a>
|
||||||
|
*/
|
||||||
|
public interface StandardClaimName {
|
||||||
|
|
||||||
|
String SUB = "sub";
|
||||||
|
|
||||||
|
String NAME = "name";
|
||||||
|
|
||||||
|
String GIVEN_NAME = "given_name";
|
||||||
|
|
||||||
|
String FAMILY_NAME = "family_name";
|
||||||
|
|
||||||
|
String MIDDLE_NAME = "middle_name";
|
||||||
|
|
||||||
|
String NICKNAME = "nickname";
|
||||||
|
|
||||||
|
String PREFERRED_USERNAME = "preferred_username";
|
||||||
|
|
||||||
|
String PROFILE = "profile";
|
||||||
|
|
||||||
|
String PICTURE = "picture";
|
||||||
|
|
||||||
|
String WEBSITE = "website";
|
||||||
|
|
||||||
|
String EMAIL = "email";
|
||||||
|
|
||||||
|
String EMAIL_VERIFIED = "email_verified";
|
||||||
|
|
||||||
|
String GENDER = "gender";
|
||||||
|
|
||||||
|
String BIRTHDATE = "birthdate";
|
||||||
|
|
||||||
|
String ZONEINFO = "zoneinfo";
|
||||||
|
|
||||||
|
String LOCALE = "locale";
|
||||||
|
|
||||||
|
String PHONE_NUMBER = "phone_number";
|
||||||
|
|
||||||
|
String PHONE_NUMBER_VERIFIED = "phone_number_verified";
|
||||||
|
|
||||||
|
String ADDRESS = "address";
|
||||||
|
|
||||||
|
String UPDATED_AT = "updated_at";
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Core classes and interfaces providing support for <i>OpenID Connect Core 1.0</i>.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.oidc;
|
|
@ -0,0 +1,154 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.oidc.user;
|
||||||
|
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
|
||||||
|
import org.springframework.security.oauth2.oidc.StandardClaimName;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.springframework.security.oauth2.oidc.StandardClaimName.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default implementation of a {@link UserInfo}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The <i>key</i> used for accessing the "name" of the
|
||||||
|
* <code>Principal</code> (user) via {@link #getAttributes()}
|
||||||
|
* is {@link StandardClaimName#NAME} or if not available
|
||||||
|
* will default to {@link StandardClaimName#SUB}.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
* @see UserInfo
|
||||||
|
* @see DefaultOAuth2User
|
||||||
|
*/
|
||||||
|
public class DefaultUserInfo extends DefaultOAuth2User implements UserInfo {
|
||||||
|
|
||||||
|
public DefaultUserInfo(Map<String, Object> attributes) {
|
||||||
|
this(Collections.emptySet(), attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DefaultUserInfo(Set<GrantedAuthority> authorities, Map<String, Object> attributes) {
|
||||||
|
super(authorities, attributes, SUB);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getSubject() {
|
||||||
|
return this.getAttributeAsString(SUB);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
String name = this.getAttributeAsString(NAME);
|
||||||
|
return (name != null ? name : super.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getGivenName() {
|
||||||
|
return this.getAttributeAsString(GIVEN_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getFamilyName() {
|
||||||
|
return this.getAttributeAsString(FAMILY_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMiddleName() {
|
||||||
|
return this.getAttributeAsString(MIDDLE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getNickName() {
|
||||||
|
return this.getAttributeAsString(NICKNAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPreferredUsername() {
|
||||||
|
return this.getAttributeAsString(PREFERRED_USERNAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getProfile() {
|
||||||
|
return this.getAttributeAsString(PROFILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPicture() {
|
||||||
|
return this.getAttributeAsString(PICTURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getWebsite() {
|
||||||
|
return this.getAttributeAsString(WEBSITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getEmail() {
|
||||||
|
return this.getAttributeAsString(EMAIL);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean getEmailVerified() {
|
||||||
|
return this.getAttributeAsBoolean(EMAIL_VERIFIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getGender() {
|
||||||
|
return this.getAttributeAsString(GENDER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getBirthdate() {
|
||||||
|
return this.getAttributeAsString(BIRTHDATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getZoneInfo() {
|
||||||
|
return this.getAttributeAsString(ZONEINFO);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getLocale() {
|
||||||
|
return this.getAttributeAsString(LOCALE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPhoneNumber() {
|
||||||
|
return this.getAttributeAsString(PHONE_NUMBER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean getPhoneNumberVerified() {
|
||||||
|
return this.getAttributeAsBoolean(PHONE_NUMBER_VERIFIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Address getAddress() {
|
||||||
|
// TODO Impl
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Instant getUpdatedAt() {
|
||||||
|
return this.getAttributeAsInstant(UPDATED_AT);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.oidc.user;
|
||||||
|
|
||||||
|
import org.springframework.security.core.AuthenticatedPrincipal;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A representation of a user <code>Principal</code>
|
||||||
|
* that is registered with an <i>OpenID Connect 1.0 Provider</i>.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The structure of the user <code>Principal</code> is defined by the
|
||||||
|
* <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#UserInfo">UserInfo Endpoint</a>,
|
||||||
|
* which is an <i>OAuth 2.0 Protected Resource</i> that returns a set of
|
||||||
|
* <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims">Claims</a>
|
||||||
|
* about the authenticated End-User.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Implementation instances of this interface represent an {@link AuthenticatedPrincipal}
|
||||||
|
* which is associated to an {@link Authentication} object
|
||||||
|
* and may be accessed via {@link Authentication#getPrincipal()}.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
* @since 5.0
|
||||||
|
* @see DefaultUserInfo
|
||||||
|
* @see AuthenticatedPrincipal
|
||||||
|
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html">OpenID Connect Core 1.0</a>
|
||||||
|
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#UserInfo">UserInfo Endpoint</a>
|
||||||
|
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims">Standard Claims</a>
|
||||||
|
*/
|
||||||
|
public interface UserInfo extends OAuth2User {
|
||||||
|
|
||||||
|
String getSubject();
|
||||||
|
|
||||||
|
String getGivenName();
|
||||||
|
|
||||||
|
String getFamilyName();
|
||||||
|
|
||||||
|
String getMiddleName();
|
||||||
|
|
||||||
|
String getNickName();
|
||||||
|
|
||||||
|
String getPreferredUsername();
|
||||||
|
|
||||||
|
String getProfile();
|
||||||
|
|
||||||
|
String getPicture();
|
||||||
|
|
||||||
|
String getWebsite();
|
||||||
|
|
||||||
|
String getEmail();
|
||||||
|
|
||||||
|
Boolean getEmailVerified();
|
||||||
|
|
||||||
|
String getGender();
|
||||||
|
|
||||||
|
String getBirthdate();
|
||||||
|
|
||||||
|
String getZoneInfo();
|
||||||
|
|
||||||
|
String getLocale();
|
||||||
|
|
||||||
|
String getPhoneNumber();
|
||||||
|
|
||||||
|
Boolean getPhoneNumberVerified();
|
||||||
|
|
||||||
|
Address getAddress();
|
||||||
|
|
||||||
|
Instant getUpdatedAt();
|
||||||
|
|
||||||
|
|
||||||
|
interface Address {
|
||||||
|
|
||||||
|
String getFormatted();
|
||||||
|
|
||||||
|
String getStreetAddress();
|
||||||
|
|
||||||
|
String getLocality();
|
||||||
|
|
||||||
|
String getRegion();
|
||||||
|
|
||||||
|
String getPostalCode();
|
||||||
|
|
||||||
|
String getCountry();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Provides a model for an <i>OpenID Connect Core 1.0</i> representation of a user <code>Principal</code>.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.oauth2.oidc.user;
|
|
@ -44,6 +44,13 @@
|
||||||
<type>pom</type>
|
<type>pom</type>
|
||||||
<scope>import</scope>
|
<scope>import</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-dependencies</artifactId>
|
||||||
|
<version>1.5.0.BUILD-SNAPSHOT</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
|
|
@ -44,6 +44,13 @@
|
||||||
<type>pom</type>
|
<type>pom</type>
|
||||||
<scope>import</scope>
|
<scope>import</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-dependencies</artifactId>
|
||||||
|
<version>1.5.0.BUILD-SNAPSHOT</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
|
|
@ -0,0 +1,342 @@
|
||||||
|
= OAuth 2.0 Login Sample
|
||||||
|
Joe Grandja
|
||||||
|
:toc:
|
||||||
|
:security-site-url: https://projects.spring.io/spring-security/
|
||||||
|
|
||||||
|
[.lead]
|
||||||
|
This guide will walk you through the steps for setting up the sample application with OAuth 2.0 Login using an external _OAuth 2.0_ or _OpenID Connect 1.0_ Provider.
|
||||||
|
The sample application is built with *Spring Boot 1.5* and the *spring-security-oauth2-client* module that is new in {security-site-url}[Spring Security 5.0].
|
||||||
|
|
||||||
|
The following sections outline detailed steps for setting up OAuth 2.0 Login with these Providers:
|
||||||
|
|
||||||
|
* <<google-login, Google>>
|
||||||
|
* <<github-login, GitHub>>
|
||||||
|
* <<facebook-login, Facebook>>
|
||||||
|
* <<okta-login, Okta>>
|
||||||
|
|
||||||
|
NOTE: The _"authentication flow"_ is realized using the *Authorization Code Grant*, as specified in the https://tools.ietf.org/html/rfc6749#section-4.1[OAuth 2.0 Authorization Framework].
|
||||||
|
|
||||||
|
[[sample-app-content]]
|
||||||
|
== Sample application content
|
||||||
|
|
||||||
|
The sample application contains the following package structure and artifacts:
|
||||||
|
|
||||||
|
*org.springframework.security.samples*
|
||||||
|
|
||||||
|
[circle]
|
||||||
|
* _OAuth2LoginApplication_ - the main class for the _Spring application_.
|
||||||
|
** *user*
|
||||||
|
*** _GitHubOAuth2User_ - a custom _UserInfo_ type for <<github-login, GitHub Login>>.
|
||||||
|
** *web*
|
||||||
|
*** _MainController_ - the root controller that displays user information after a successful login.
|
||||||
|
|
||||||
|
*org.springframework.boot.autoconfigure.security.oauth2.client*
|
||||||
|
|
||||||
|
[circle]
|
||||||
|
* <<client-registration-auto-configuration-class, _ClientRegistrationAutoConfiguration_>> - a Spring Boot auto-configuration class
|
||||||
|
that automatically registers a _ClientRegistrationRepository_ bean in the _ApplicationContext_.
|
||||||
|
* <<oauth2-login-auto-configuration-class, _OAuth2LoginAutoConfiguration_>> - a Spring Boot auto-configuration class that automatically enables OAuth 2.0 Login.
|
||||||
|
|
||||||
|
WARNING: The Spring Boot auto-configuration classes (and dependent resources) will eventually _live_ in the *Spring Boot Security Starter*.
|
||||||
|
|
||||||
|
NOTE: See <<oauth2-login-auto-configuration, OAuth 2.0 Login auto-configuration>> for a detailed overview of the auto-configuration classes.
|
||||||
|
|
||||||
|
[[google-login]]
|
||||||
|
== Setting up *_Login with Google_*
|
||||||
|
|
||||||
|
The goal for this section of the guide is to setup login using Google as the _Authentication Provider_.
|
||||||
|
|
||||||
|
NOTE: https://developers.google.com/identity/protocols/OpenIDConnect[Google's OAuth 2.0 implementation] for authentication conforms to the
|
||||||
|
http://openid.net/connect/[OpenID Connect] specification and is http://openid.net/certification/[OpenID Certified].
|
||||||
|
|
||||||
|
[[google-login-register-credentials]]
|
||||||
|
=== Register OAuth 2.0 credentials
|
||||||
|
|
||||||
|
In order to use Google's OAuth 2.0 authentication system for login, you must set up a project in the *Google API Console* to obtain OAuth 2.0 credentials.
|
||||||
|
|
||||||
|
Follow the instructions on the https://developers.google.com/identity/protocols/OpenIDConnect[OpenID Connect] page starting in the section *_"Setting up OAuth 2.0"_*.
|
||||||
|
|
||||||
|
After completing the sub-section, *_"Obtain OAuth 2.0 credentials"_*, you should have created a new *OAuth Client* with credentials consisting of a *Client ID* and *Client Secret*.
|
||||||
|
|
||||||
|
[[google-login-redirect-uri]]
|
||||||
|
=== Setting the redirect URI
|
||||||
|
|
||||||
|
The redirect URI is the path in the sample application that the end-user's user-agent is redirected back to after they have authenticated with Google
|
||||||
|
and have granted access to the OAuth Client _(created from the <<google-login-register-credentials, previous step>>)_ on the *Consent screen* page.
|
||||||
|
|
||||||
|
For the sub-section, *_"Set a redirect URI"_*, ensure the *Authorised redirect URIs* is set to *http://localhost:8080/oauth2/authorize/code/google*
|
||||||
|
|
||||||
|
TIP: The default redirect URI is *_"{scheme}://{serverName}:{serverPort}/oauth2/authorize/code/{clientAlias}"_*.
|
||||||
|
See <<oauth2-client-properties, OAuth client properties>> for more details on this default.
|
||||||
|
|
||||||
|
[[google-login-configure-application-yml]]
|
||||||
|
=== Configuring application.yml
|
||||||
|
|
||||||
|
Now that we have created a new OAuth Client with Google, we need to configure the sample application to use this OAuth Client for the _authentication flow_.
|
||||||
|
|
||||||
|
Go to *_src/main/resources_* and edit *application.yml*. Add the following configuration:
|
||||||
|
|
||||||
|
[source,yaml]
|
||||||
|
----
|
||||||
|
security:
|
||||||
|
oauth2:
|
||||||
|
client:
|
||||||
|
google:
|
||||||
|
client-id: ${client-id}
|
||||||
|
client-secret: ${client-secret}
|
||||||
|
----
|
||||||
|
|
||||||
|
Replace *${client-id}* and *${client-secret}* with the OAuth 2.0 credentials created in the previous section <<google-login-register-credentials, Register OAuth 2.0 credentials>>.
|
||||||
|
|
||||||
|
[TIP]
|
||||||
|
.OAuth client properties
|
||||||
|
====
|
||||||
|
. *security.oauth2.client* is the *_base property prefix_* for OAuth client properties.
|
||||||
|
. Just below the *_base property prefix_* is the *_client property key_*, for example *security.oauth2.client.google*.
|
||||||
|
. At the base of the *_client property key_* are the properties for specifying the configuration for an OAuth Client.
|
||||||
|
A list of these properties are detailed in <<oauth2-client-properties, OAuth client properties>>.
|
||||||
|
====
|
||||||
|
|
||||||
|
[[google-login-run-sample]]
|
||||||
|
=== Running the sample
|
||||||
|
|
||||||
|
Launch the Spring Boot application by running *_org.springframework.security.samples.OAuth2LoginApplication_*.
|
||||||
|
|
||||||
|
After the application successfully starts up, go to http://localhost:8080. You will be redirected to http://localhost:8080/login, which will display an _auto-generated login page_ with an anchor link for *Google*.
|
||||||
|
|
||||||
|
Click through on the Google link and you'll be redirected to Google for authentication.
|
||||||
|
|
||||||
|
After you authenticate using your Google credentials, the next page presented to you will be the *Consent screen*.
|
||||||
|
The Consent screen will ask you to either *_Allow_* or *_Deny_* access to the OAuth Client you created in the previous step <<google-login-register-credentials, Register OAuth 2.0 credentials>>.
|
||||||
|
Click *_Allow_* to authorize the OAuth Client to access your _email address_ and _basic profile_ information.
|
||||||
|
|
||||||
|
At this point, the OAuth Client will retrieve your email address and basic profile information from the http://openid.net/specs/openid-connect-core-1_0.html#UserInfo[*UserInfo Endpoint*] and establish an _authenticated session_.
|
||||||
|
The home page will then be displayed showing the user attributes retrieved from the *UserInfo Endpoint*, for example, name, email, profile, sub, etc.
|
||||||
|
|
||||||
|
[[oauth2-login-auto-configuration]]
|
||||||
|
== OAuth 2.0 Login auto-configuration
|
||||||
|
|
||||||
|
As you worked through this guide and setup OAuth 2.0 Login with one of the Providers,
|
||||||
|
we hope you noticed the ease in configuration and setup required in getting the sample up and running?
|
||||||
|
And you may be asking, how does this all work? Thanks to some Spring Boot auto-configuration _magic_,
|
||||||
|
we were able to automatically register the OAuth Client(s) configured in the `Environment`,
|
||||||
|
as well, provide a minimal security configuration for OAuth 2.0 Login for these registered OAuth Client(s).
|
||||||
|
|
||||||
|
The following provides an overview of the Spring Boot auto-configuration classes:
|
||||||
|
|
||||||
|
[[client-registration-auto-configuration-class]]
|
||||||
|
*_org.springframework.boot.autoconfigure.security.oauth2.client.ClientRegistrationAutoConfiguration_*::
|
||||||
|
`ClientRegistrationAutoConfiguration` is responsible for registering a `ClientRegistrationRepository` _bean_ with the `ApplicationContext`.
|
||||||
|
The `ClientRegistrationRepository` is composed of one or more `ClientRegistration` instances, which are created from the OAuth client properties
|
||||||
|
configured in the `Environment` that are prefixed with `security.oauth2.client.[client-alias]`, for example, `security.oauth2.client.google`.
|
||||||
|
|
||||||
|
NOTE: `ClientRegistrationAutoConfiguration` also loads a _resource_ named *oauth2-clients-defaults.yml*,
|
||||||
|
which provides a set of default client property values for a number of _well-known_ Providers.
|
||||||
|
More on this in the later section <<oauth2-default-client-properties, Default client property values>>.
|
||||||
|
|
||||||
|
[[oauth2-login-auto-configuration-class]]
|
||||||
|
*_org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2LoginAutoConfiguration_*::
|
||||||
|
`OAuth2LoginAutoConfiguration` is responsible for enabling OAuth 2.0 Login,
|
||||||
|
only if there is a `ClientRegistrationRepository` _bean_ available in the `ApplicationContext`.
|
||||||
|
|
||||||
|
WARNING: The auto-configuration classes (and dependent resources) will eventually _live_ in the *Spring Boot Security Starter*.
|
||||||
|
|
||||||
|
[[oauth2-client-properties]]
|
||||||
|
=== OAuth client properties
|
||||||
|
|
||||||
|
The following specifies the common set of properties available for configuring an OAuth Client.
|
||||||
|
|
||||||
|
[TIP]
|
||||||
|
====
|
||||||
|
- *security.oauth2.client* is the *_base property prefix_* for OAuth client properties.
|
||||||
|
- Just below the *_base property prefix_* is the *_client property key_*, for example *security.oauth2.client.google*.
|
||||||
|
- At the base of the *_client property key_* are the properties for specifying the configuration for an OAuth Client.
|
||||||
|
====
|
||||||
|
|
||||||
|
- *client-authentication-method* - the method used to authenticate the _Client_ with the _Provider_. Supported values are *header* and *form*.
|
||||||
|
- *authorized-grant-type* - the OAuth 2.0 Authorization Framework defines the https://tools.ietf.org/html/rfc6749#section-1.3.1[Authorization Code] grant type,
|
||||||
|
which is used to realize the _"authentication flow"_. Currently, this is the only supported grant type.
|
||||||
|
- *redirect-uri* - this is the client's _registered_ redirect URI that the _Authorization Server_ redirects the end-user's user-agent
|
||||||
|
to after the end-user has authenticated and authorized access for the client.
|
||||||
|
|
||||||
|
NOTE: The default redirect URI is _"{scheme}://{serverName}:{serverPort}/oauth2/authorize/code/{clientAlias}"_, which leverages *URI template variables*.
|
||||||
|
|
||||||
|
- *scopes* - a comma-delimited string of scope(s) requested during the _Authorization Request_ flow, for example: _openid, email, profile_
|
||||||
|
|
||||||
|
NOTE: _OpenID Connect 1.0_ defines these http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims[standard scopes]: _profile, email, address, phone_
|
||||||
|
|
||||||
|
NOTE: Non-standard scopes may be defined by a standard _OAuth 2.0 Provider_. Please consult the Provider's OAuth API documentation to learn which scopes are supported.
|
||||||
|
|
||||||
|
- *authorization-uri* - the URI used by the client to redirect the end-user's user-agent to the _Authorization Server_ in order to obtain authorization from the end-user (the _Resource Owner_).
|
||||||
|
- *token-uri* - the URI used by the client when exchanging an _Authorization Grant_ (for example, Authorization Code) for an _Access Token_ at the _Authorization Server_.
|
||||||
|
- *user-info-uri* - the URI used by the client to access the protected resource *UserInfo Endpoint*, in order to obtain attributes of the end-user.
|
||||||
|
- *user-info-converter* - the `Converter` implementation class used to convert the *UserInfo Response* to a `UserInfo` (_OpenID Connect 1.0 Provider_) or `OAuth2User` instance (_Standard OAuth 2.0 Provider_).
|
||||||
|
|
||||||
|
TIP: The `Converter` implementation class for an _OpenID Connect 1.0 Provider_ is *org.springframework.security.oauth2.client.user.converter.UserInfoConverter*
|
||||||
|
and for a standard _OAuth 2.0 Provider_ it's *org.springframework.security.oauth2.client.user.converter.OAuth2UserConverter*.
|
||||||
|
|
||||||
|
- *user-info-name-attribute-key* - the _key_ used to retrieve the *Name* of the end-user from the `Map` of available attributes in `UserInfo` or `OAuth2User`.
|
||||||
|
|
||||||
|
NOTE: _OpenID Connect 1.0_ defines the http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims[*"name"* Claim], which is the end-user's full name and is the default used for `UserInfo`.
|
||||||
|
|
||||||
|
IMPORTANT: Standard _OAuth 2.0 Provider's_ may vary the naming of their *Name* attribute. Please consult the Provider's *UserInfo* API documentation.
|
||||||
|
This is a *_required_* property when *user-info-converter* is set to `OAuth2UserConverter`.
|
||||||
|
|
||||||
|
- *client-name* - this is a descriptive name used for the client. The name may be used in certain scenarios, for example, when displaying the name of the client in the _auto-generated login page_.
|
||||||
|
- *client-alias* - an _alias_ which uniquely identifies the client. It *must be* unique within a `ClientRegistrationRepository`.
|
||||||
|
|
||||||
|
[[oauth2-default-client-properties]]
|
||||||
|
=== Default client property values
|
||||||
|
|
||||||
|
As noted previously, <<client-registration-auto-configuration-class, `ClientRegistrationAutoConfiguration`>> loads a _resource_ named *oauth2-clients-defaults.yml*,
|
||||||
|
which provides a set of default client property values for a number of _well-known_ Providers.
|
||||||
|
|
||||||
|
For example, the *authorization-uri*, *token-uri*, *user-info-uri* rarely change for a Provider and therefore it makes sense to
|
||||||
|
provide a set of defaults in order to reduce the configuration required by the user.
|
||||||
|
|
||||||
|
Below are the current set of default client property values:
|
||||||
|
|
||||||
|
.oauth2-clients-defaults.yml
|
||||||
|
[source,yaml]
|
||||||
|
----
|
||||||
|
security:
|
||||||
|
oauth2:
|
||||||
|
client:
|
||||||
|
google:
|
||||||
|
client-authentication-method: header
|
||||||
|
authorized-grant-type: authorization_code
|
||||||
|
redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
|
||||||
|
scopes: openid, email, profile
|
||||||
|
authorization-uri: "https://accounts.google.com/o/oauth2/auth"
|
||||||
|
token-uri: "https://accounts.google.com/o/oauth2/token"
|
||||||
|
user-info-uri: "https://www.googleapis.com/oauth2/v3/userinfo"
|
||||||
|
user-info-converter: "org.springframework.security.oauth2.client.user.converter.UserInfoConverter"
|
||||||
|
client-name: Google
|
||||||
|
client-alias: google
|
||||||
|
github:
|
||||||
|
client-authentication-method: header
|
||||||
|
authorized-grant-type: authorization_code
|
||||||
|
redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
|
||||||
|
scopes: user
|
||||||
|
authorization-uri: "https://github.com/login/oauth/authorize"
|
||||||
|
token-uri: "https://github.com/login/oauth/access_token"
|
||||||
|
user-info-uri: "https://api.github.com/user"
|
||||||
|
user-info-converter: "org.springframework.security.oauth2.client.user.converter.OAuth2UserConverter"
|
||||||
|
client-name: GitHub
|
||||||
|
client-alias: github
|
||||||
|
facebook:
|
||||||
|
client-authentication-method: form
|
||||||
|
authorized-grant-type: authorization_code
|
||||||
|
redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
|
||||||
|
scopes: public_profile, email
|
||||||
|
authorization-uri: "https://www.facebook.com/v2.8/dialog/oauth"
|
||||||
|
token-uri: "https://graph.facebook.com/v2.8/oauth/access_token"
|
||||||
|
user-info-uri: "https://graph.facebook.com/me"
|
||||||
|
user-info-converter: "org.springframework.security.oauth2.client.user.converter.OAuth2UserConverter"
|
||||||
|
client-name: Facebook
|
||||||
|
client-alias: facebook
|
||||||
|
okta:
|
||||||
|
client-authentication-method: header
|
||||||
|
authorized-grant-type: authorization_code
|
||||||
|
redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
|
||||||
|
scopes: openid, email, profile
|
||||||
|
user-info-converter: "org.springframework.security.oauth2.client.user.converter.UserInfoConverter"
|
||||||
|
client-name: Okta
|
||||||
|
client-alias: okta
|
||||||
|
----
|
||||||
|
|
||||||
|
= Appendix
|
||||||
|
'''
|
||||||
|
|
||||||
|
[[configure-non-spring-boot-app]]
|
||||||
|
== Configuring a _Non-Spring-Boot_ application
|
||||||
|
|
||||||
|
If you are not using Spring Boot for your application, you will not be able to leverage the auto-configuration features for OAuth 2.0 Login.
|
||||||
|
You will be required to provide your own _security configuration_ in order to enable OAuth 2.0 Login.
|
||||||
|
|
||||||
|
The following sample code demonstrates a minimal security configuration for enabling OAuth 2.0 Login.
|
||||||
|
|
||||||
|
Assuming we have a _properties file_ named *oauth2-clients.properties* on the _classpath_ and it specifies all the _required_ properties for an OAuth Client, specifically _"Google"_:
|
||||||
|
|
||||||
|
.oauth2-clients.properties
|
||||||
|
[source,properties]
|
||||||
|
----
|
||||||
|
security.oauth2.client.google.client-id=${client-id}
|
||||||
|
security.oauth2.client.google.client-secret=${client-secret}
|
||||||
|
security.oauth2.client.google.client-authentication-method=header
|
||||||
|
security.oauth2.client.google.authorized-grant-type=authorization_code
|
||||||
|
security.oauth2.client.google.redirect-uri=http://localhost:8080/oauth2/authorize/code/google
|
||||||
|
security.oauth2.client.google.scopes=openid,email,profile
|
||||||
|
security.oauth2.client.google.authorization-uri=https://accounts.google.com/o/oauth2/auth
|
||||||
|
security.oauth2.client.google.token-uri=https://accounts.google.com/o/oauth2/token
|
||||||
|
security.oauth2.client.google.user-info-uri=https://www.googleapis.com/oauth2/v3/userinfo
|
||||||
|
security.oauth2.client.google.user-info-converter=org.springframework.security.oauth2.client.user.converter.UserInfoConverter
|
||||||
|
security.oauth2.client.google.client-name=Google
|
||||||
|
security.oauth2.client.google.client-alias=google
|
||||||
|
----
|
||||||
|
|
||||||
|
The following _security configuration_ will enable OAuth 2.0 Login using _"Google"_ as the _Authentication Provider_:
|
||||||
|
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
@EnableWebSecurity
|
||||||
|
@PropertySource("classpath:oauth2-clients.properties")
|
||||||
|
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||||
|
private Environment environment;
|
||||||
|
|
||||||
|
public SecurityConfig(Environment environment) {
|
||||||
|
this.environment = environment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void configure(HttpSecurity http) throws Exception {
|
||||||
|
http
|
||||||
|
.authorizeRequests()
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
.and()
|
||||||
|
.oauth2Login()
|
||||||
|
.clients(clientRegistrationRepository())
|
||||||
|
.userInfoEndpoint()
|
||||||
|
.userInfoTypeConverter(
|
||||||
|
new UserInfoConverter(),
|
||||||
|
new URI("https://www.googleapis.com/oauth2/v3/userinfo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ClientRegistrationRepository clientRegistrationRepository() {
|
||||||
|
List<ClientRegistration> clientRegistrations = Collections.singletonList(
|
||||||
|
clientRegistration("security.oauth2.client.google."));
|
||||||
|
|
||||||
|
return new InMemoryClientRegistrationRepository(clientRegistrations);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClientRegistration clientRegistration(String clientPropertyKey) {
|
||||||
|
String clientId = this.environment.getProperty(clientPropertyKey + "client-id");
|
||||||
|
String clientSecret = this.environment.getProperty(clientPropertyKey + "client-secret");
|
||||||
|
ClientAuthenticationMethod clientAuthenticationMethod = ClientAuthenticationMethod.valueOf(
|
||||||
|
this.environment.getProperty(clientPropertyKey + "client-authentication-method").toUpperCase());
|
||||||
|
AuthorizationGrantType authorizationGrantType = AuthorizationGrantType.valueOf(
|
||||||
|
this.environment.getProperty(clientPropertyKey + "authorized-grant-type").toUpperCase());
|
||||||
|
String redirectUri = this.environment.getProperty(clientPropertyKey + "redirect-uri");
|
||||||
|
String[] scopes = this.environment.getProperty(clientPropertyKey + "scopes").split(",");
|
||||||
|
String authorizationUri = this.environment.getProperty(clientPropertyKey + "authorization-uri");
|
||||||
|
String tokenUri = this.environment.getProperty(clientPropertyKey + "token-uri");
|
||||||
|
String userInfoUri = this.environment.getProperty(clientPropertyKey + "user-info-uri");
|
||||||
|
String clientName = this.environment.getProperty(clientPropertyKey + "client-name");
|
||||||
|
String clientAlias = this.environment.getProperty(clientPropertyKey + "client-alias");
|
||||||
|
|
||||||
|
return new ClientRegistration.Builder(clientId)
|
||||||
|
.clientSecret(clientSecret)
|
||||||
|
.clientAuthenticationMethod(clientAuthenticationMethod)
|
||||||
|
.authorizedGrantType(authorizationGrantType)
|
||||||
|
.redirectUri(redirectUri)
|
||||||
|
.scopes(scopes)
|
||||||
|
.authorizationUri(authorizationUri)
|
||||||
|
.tokenUri(tokenUri)
|
||||||
|
.userInfoUri(userInfoUri)
|
||||||
|
.clientName(clientName)
|
||||||
|
.clientAlias(clientAlias)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----
|
|
@ -0,0 +1,173 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-samples-boot-oauth2login</artifactId>
|
||||||
|
<version>5.0.0.BUILD-SNAPSHOT</version>
|
||||||
|
<name>spring-security-samples-boot-oauth2login</name>
|
||||||
|
<description>spring-security-samples-boot-oauth2login</description>
|
||||||
|
<url>http://spring.io/spring-security</url>
|
||||||
|
<organization>
|
||||||
|
<name>spring.io</name>
|
||||||
|
<url>http://spring.io/</url>
|
||||||
|
</organization>
|
||||||
|
<licenses>
|
||||||
|
<license>
|
||||||
|
<name>The Apache Software License, Version 2.0</name>
|
||||||
|
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
|
||||||
|
<distribution>repo</distribution>
|
||||||
|
</license>
|
||||||
|
</licenses>
|
||||||
|
<developers>
|
||||||
|
<developer>
|
||||||
|
<id>rwinch</id>
|
||||||
|
<name>Rob Winch</name>
|
||||||
|
<email>rwinch@pivotal.io</email>
|
||||||
|
</developer>
|
||||||
|
<developer>
|
||||||
|
<id>jgrandja</id>
|
||||||
|
<name>Joe Grandja</name>
|
||||||
|
<email>jgrandja@pivotal.io</email>
|
||||||
|
</developer>
|
||||||
|
</developers>
|
||||||
|
<scm>
|
||||||
|
<connection>scm:git:git://github.com/spring-projects/spring-security</connection>
|
||||||
|
<developerConnection>scm:git:git://github.com/spring-projects/spring-security</developerConnection>
|
||||||
|
<url>https://github.com/spring-projects/spring-security</url>
|
||||||
|
</scm>
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework</groupId>
|
||||||
|
<artifactId>spring-framework-bom</artifactId>
|
||||||
|
<version>4.3.5.RELEASE</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-dependencies</artifactId>
|
||||||
|
<version>1.5.0.BUILD-SNAPSHOT</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-config</artifactId>
|
||||||
|
<version>5.0.0.BUILD-SNAPSHOT</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-oauth2-client</artifactId>
|
||||||
|
<version>5.0.0.BUILD-SNAPSHOT</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-web</artifactId>
|
||||||
|
<version>5.0.0.BUILD-SNAPSHOT</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.thymeleaf.extras</groupId>
|
||||||
|
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
|
||||||
|
<version>2.1.3.RELEASE</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>commons-logging</groupId>
|
||||||
|
<artifactId>commons-logging</artifactId>
|
||||||
|
<version>1.2</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>ch.qos.logback</groupId>
|
||||||
|
<artifactId>logback-classic</artifactId>
|
||||||
|
<version>1.1.2</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<version>4.12</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.sourceforge.htmlunit</groupId>
|
||||||
|
<artifactId>htmlunit</artifactId>
|
||||||
|
<version>2.24</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.assertj</groupId>
|
||||||
|
<artifactId>assertj-core</artifactId>
|
||||||
|
<version>3.6.2</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-core</artifactId>
|
||||||
|
<version>1.10.19</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>jcl-over-slf4j</artifactId>
|
||||||
|
<version>1.7.7</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-test</artifactId>
|
||||||
|
<version>5.0.0.BUILD-SNAPSHOT</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework</groupId>
|
||||||
|
<artifactId>spring-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>spring-snapshot</id>
|
||||||
|
<url>https://repo.spring.io/snapshot</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<source>1.8</source>
|
||||||
|
<target>1.8</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
|
@ -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'
|
||||||
|
}
|
|
@ -0,0 +1,405 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.samples;
|
||||||
|
|
||||||
|
import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
|
||||||
|
import com.gargoylesoftware.htmlunit.WebClient;
|
||||||
|
import com.gargoylesoftware.htmlunit.WebResponse;
|
||||||
|
import com.gargoylesoftware.htmlunit.html.DomNodeList;
|
||||||
|
import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
|
||||||
|
import com.gargoylesoftware.htmlunit.html.HtmlElement;
|
||||||
|
import com.gargoylesoftware.htmlunit.html.HtmlPage;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.SpringBootConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.context.annotation.ComponentScan;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationProcessingFilter;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeRequestRedirectFilter;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.AuthorizationGrantTokenExchanger;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||||
|
import org.springframework.security.oauth2.client.user.OAuth2UserService;
|
||||||
|
import org.springframework.security.oauth2.core.AccessToken;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2Parameter;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.ResponseType;
|
||||||
|
import org.springframework.security.oauth2.core.endpoint.TokenResponseAttributes;
|
||||||
|
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
|
||||||
|
import org.springframework.test.context.junit4.SpringRunner;
|
||||||
|
import org.springframework.web.util.UriComponents;
|
||||||
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLDecoder;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Matchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests for the OAuth 2.0 client filters {@link AuthorizationCodeRequestRedirectFilter}
|
||||||
|
* and {@link AuthorizationCodeAuthenticationProcessingFilter}.
|
||||||
|
* These filters work together to realize the Authorization Code Grant flow.
|
||||||
|
*
|
||||||
|
* @author Joe Grandja
|
||||||
|
*/
|
||||||
|
@RunWith(SpringRunner.class)
|
||||||
|
@SpringBootTest
|
||||||
|
@AutoConfigureMockMvc
|
||||||
|
public class OAuth2LoginApplicationTests {
|
||||||
|
private static final String AUTHORIZATION_BASE_URI = "/oauth2/authorization/code";
|
||||||
|
private static final String AUTHORIZE_BASE_URL = "http://localhost:8080/oauth2/authorize/code";
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private WebClient webClient;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ClientRegistrationRepository clientRegistrationRepository;
|
||||||
|
|
||||||
|
private ClientRegistration googleClientRegistration;
|
||||||
|
private ClientRegistration githubClientRegistration;
|
||||||
|
private ClientRegistration facebookClientRegistration;
|
||||||
|
private ClientRegistration oktaClientRegistration;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() {
|
||||||
|
this.webClient.getCookieManager().clearCookies();
|
||||||
|
this.googleClientRegistration = this.clientRegistrationRepository.getRegistrationByClientAlias("google");
|
||||||
|
this.githubClientRegistration = this.clientRegistrationRepository.getRegistrationByClientAlias("github");
|
||||||
|
this.facebookClientRegistration = this.clientRegistrationRepository.getRegistrationByClientAlias("facebook");
|
||||||
|
this.oktaClientRegistration = this.clientRegistrationRepository.getRegistrationByClientAlias("okta");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestRootPageWhenNotAuthenticatedThenDisplayLoginPage() throws Exception {
|
||||||
|
HtmlPage page = this.webClient.getPage("/");
|
||||||
|
this.assertLoginPage(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestOtherPageWhenNotAuthenticatedThenDisplayLoginPage() throws Exception {
|
||||||
|
HtmlPage page = this.webClient.getPage("/other-page");
|
||||||
|
this.assertLoginPage(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestAuthorizeGitHubClientWhenLinkClickedThenStatusRedirectForAuthorization() throws Exception {
|
||||||
|
HtmlPage page = this.webClient.getPage("/");
|
||||||
|
|
||||||
|
HtmlAnchor clientAnchorElement = this.getClientAnchorElement(page, this.githubClientRegistration);
|
||||||
|
assertThat(clientAnchorElement).isNotNull();
|
||||||
|
|
||||||
|
WebResponse response = this.followLinkDisableRedirects(clientAnchorElement);
|
||||||
|
|
||||||
|
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.MOVED_PERMANENTLY.value());
|
||||||
|
|
||||||
|
String authorizeRedirectUri = response.getResponseHeaderValue("Location");
|
||||||
|
assertThat(authorizeRedirectUri).isNotNull();
|
||||||
|
|
||||||
|
UriComponents uriComponents = UriComponentsBuilder.fromUri(URI.create(authorizeRedirectUri)).build();
|
||||||
|
|
||||||
|
String requestUri = uriComponents.getScheme() + "://" + uriComponents.getHost() + uriComponents.getPath();
|
||||||
|
assertThat(requestUri).isEqualTo(this.githubClientRegistration.getProviderDetails().getAuthorizationUri().toString());
|
||||||
|
|
||||||
|
Map<String, String> params = uriComponents.getQueryParams().toSingleValueMap();
|
||||||
|
|
||||||
|
assertThat(params.get(OAuth2Parameter.RESPONSE_TYPE)).isEqualTo(ResponseType.CODE.value());
|
||||||
|
assertThat(params.get(OAuth2Parameter.CLIENT_ID)).isEqualTo(this.githubClientRegistration.getClientId());
|
||||||
|
String redirectUri = AUTHORIZE_BASE_URL + "/" + this.githubClientRegistration.getClientAlias();
|
||||||
|
assertThat(URLDecoder.decode(params.get(OAuth2Parameter.REDIRECT_URI), "UTF-8")).isEqualTo(redirectUri);
|
||||||
|
assertThat(URLDecoder.decode(params.get(OAuth2Parameter.SCOPE), "UTF-8"))
|
||||||
|
.isEqualTo(this.githubClientRegistration.getScopes().stream().collect(Collectors.joining(" ")));
|
||||||
|
assertThat(params.get(OAuth2Parameter.STATE)).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestAuthorizeClientWhenInvalidClientThenStatusBadRequest() throws Exception {
|
||||||
|
HtmlPage page = this.webClient.getPage("/");
|
||||||
|
|
||||||
|
HtmlAnchor clientAnchorElement = this.getClientAnchorElement(page, this.googleClientRegistration);
|
||||||
|
assertThat(clientAnchorElement).isNotNull();
|
||||||
|
clientAnchorElement.setAttribute("href", clientAnchorElement.getHrefAttribute() + "-invalid");
|
||||||
|
|
||||||
|
WebResponse response = null;
|
||||||
|
try {
|
||||||
|
clientAnchorElement.click();
|
||||||
|
} catch (FailingHttpStatusCodeException ex) {
|
||||||
|
response = ex.getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestAuthorizationCodeGrantWhenValidAuthorizationResponseThenDisplayUserInfoPage() throws Exception {
|
||||||
|
HtmlPage page = this.webClient.getPage("/");
|
||||||
|
|
||||||
|
HtmlAnchor clientAnchorElement = this.getClientAnchorElement(page, this.githubClientRegistration);
|
||||||
|
assertThat(clientAnchorElement).isNotNull();
|
||||||
|
|
||||||
|
WebResponse response = this.followLinkDisableRedirects(clientAnchorElement);
|
||||||
|
|
||||||
|
UriComponents authorizeRequestUriComponents = UriComponentsBuilder.fromUri(
|
||||||
|
URI.create(response.getResponseHeaderValue("Location"))).build();
|
||||||
|
|
||||||
|
Map<String, String> params = authorizeRequestUriComponents.getQueryParams().toSingleValueMap();
|
||||||
|
String code = "auth-code";
|
||||||
|
String state = URLDecoder.decode(params.get(OAuth2Parameter.STATE), "UTF-8");
|
||||||
|
String redirectUri = URLDecoder.decode(params.get(OAuth2Parameter.REDIRECT_URI), "UTF-8");
|
||||||
|
|
||||||
|
String authorizationResponseUri =
|
||||||
|
UriComponentsBuilder.fromHttpUrl(redirectUri)
|
||||||
|
.queryParam(OAuth2Parameter.CODE, code)
|
||||||
|
.queryParam(OAuth2Parameter.STATE, state)
|
||||||
|
.build().encode().toUriString();
|
||||||
|
|
||||||
|
page = this.webClient.getPage(new URL(authorizationResponseUri));
|
||||||
|
this.assertUserInfoPage(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestAuthorizationCodeGrantWhenNoMatchingAuthorizationRequestThenDisplayLoginPageWithError() throws Exception {
|
||||||
|
HtmlPage page = this.webClient.getPage("/");
|
||||||
|
URL loginPageUrl = page.getBaseURL();
|
||||||
|
URL loginErrorPageUrl = new URL(loginPageUrl.toString() + "?error");
|
||||||
|
|
||||||
|
String code = "auth-code";
|
||||||
|
String state = "state";
|
||||||
|
String redirectUri = AUTHORIZE_BASE_URL + "/" + this.googleClientRegistration.getClientAlias();
|
||||||
|
|
||||||
|
String authorizationResponseUri =
|
||||||
|
UriComponentsBuilder.fromHttpUrl(redirectUri)
|
||||||
|
.queryParam(OAuth2Parameter.CODE, code)
|
||||||
|
.queryParam(OAuth2Parameter.STATE, state)
|
||||||
|
.build().encode().toUriString();
|
||||||
|
|
||||||
|
// Clear session cookie will ensure the 'session-saved'
|
||||||
|
// Authorization Request (from previous request) is not found
|
||||||
|
this.webClient.getCookieManager().clearCookies();
|
||||||
|
|
||||||
|
page = this.webClient.getPage(new URL(authorizationResponseUri));
|
||||||
|
assertThat(page.getBaseURL()).isEqualTo(loginErrorPageUrl);
|
||||||
|
|
||||||
|
HtmlElement errorElement = page.getBody().getFirstByXPath("p");
|
||||||
|
assertThat(errorElement).isNotNull();
|
||||||
|
assertThat(errorElement.asText()).contains("authorization_request_not_found");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestAuthorizationCodeGrantWhenInvalidStateParamThenDisplayLoginPageWithError() throws Exception {
|
||||||
|
HtmlPage page = this.webClient.getPage("/");
|
||||||
|
URL loginPageUrl = page.getBaseURL();
|
||||||
|
URL loginErrorPageUrl = new URL(loginPageUrl.toString() + "?error");
|
||||||
|
|
||||||
|
HtmlAnchor clientAnchorElement = this.getClientAnchorElement(page, this.googleClientRegistration);
|
||||||
|
assertThat(clientAnchorElement).isNotNull();
|
||||||
|
this.followLinkDisableRedirects(clientAnchorElement);
|
||||||
|
|
||||||
|
String code = "auth-code";
|
||||||
|
String state = "invalid-state";
|
||||||
|
String redirectUri = AUTHORIZE_BASE_URL + "/" + this.githubClientRegistration.getClientAlias();
|
||||||
|
|
||||||
|
String authorizationResponseUri =
|
||||||
|
UriComponentsBuilder.fromHttpUrl(redirectUri)
|
||||||
|
.queryParam(OAuth2Parameter.CODE, code)
|
||||||
|
.queryParam(OAuth2Parameter.STATE, state)
|
||||||
|
.build().encode().toUriString();
|
||||||
|
|
||||||
|
page = this.webClient.getPage(new URL(authorizationResponseUri));
|
||||||
|
assertThat(page.getBaseURL()).isEqualTo(loginErrorPageUrl);
|
||||||
|
|
||||||
|
HtmlElement errorElement = page.getBody().getFirstByXPath("p");
|
||||||
|
assertThat(errorElement).isNotNull();
|
||||||
|
assertThat(errorElement.asText()).contains("invalid_state_parameter");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestAuthorizationCodeGrantWhenInvalidRedirectUriThenDisplayLoginPageWithError() throws Exception {
|
||||||
|
HtmlPage page = this.webClient.getPage("/");
|
||||||
|
URL loginPageUrl = page.getBaseURL();
|
||||||
|
URL loginErrorPageUrl = new URL(loginPageUrl.toString() + "?error");
|
||||||
|
|
||||||
|
HtmlAnchor clientAnchorElement = this.getClientAnchorElement(page, this.googleClientRegistration);
|
||||||
|
assertThat(clientAnchorElement).isNotNull();
|
||||||
|
|
||||||
|
WebResponse response = this.followLinkDisableRedirects(clientAnchorElement);
|
||||||
|
|
||||||
|
UriComponents authorizeRequestUriComponents = UriComponentsBuilder.fromUri(
|
||||||
|
URI.create(response.getResponseHeaderValue("Location"))).build();
|
||||||
|
|
||||||
|
Map<String, String> params = authorizeRequestUriComponents.getQueryParams().toSingleValueMap();
|
||||||
|
String code = "auth-code";
|
||||||
|
String state = URLDecoder.decode(params.get(OAuth2Parameter.STATE), "UTF-8");
|
||||||
|
String redirectUri = URLDecoder.decode(params.get(OAuth2Parameter.REDIRECT_URI), "UTF-8");
|
||||||
|
redirectUri += "-invalid";
|
||||||
|
|
||||||
|
String authorizationResponseUri =
|
||||||
|
UriComponentsBuilder.fromHttpUrl(redirectUri)
|
||||||
|
.queryParam(OAuth2Parameter.CODE, code)
|
||||||
|
.queryParam(OAuth2Parameter.STATE, state)
|
||||||
|
.build().encode().toUriString();
|
||||||
|
|
||||||
|
page = this.webClient.getPage(new URL(authorizationResponseUri));
|
||||||
|
assertThat(page.getBaseURL()).isEqualTo(loginErrorPageUrl);
|
||||||
|
|
||||||
|
HtmlElement errorElement = page.getBody().getFirstByXPath("p");
|
||||||
|
assertThat(errorElement).isNotNull();
|
||||||
|
assertThat(errorElement.asText()).contains("invalid_redirect_uri_parameter");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestAuthorizationCodeGrantWhenStandardErrorCodeResponseThenDisplayLoginPageWithError() throws Exception {
|
||||||
|
HtmlPage page = this.webClient.getPage("/");
|
||||||
|
URL loginPageUrl = page.getBaseURL();
|
||||||
|
URL loginErrorPageUrl = new URL(loginPageUrl.toString() + "?error");
|
||||||
|
|
||||||
|
String error = OAuth2Error.INVALID_CLIENT_ERROR_CODE;
|
||||||
|
String state = "state";
|
||||||
|
String redirectUri = AUTHORIZE_BASE_URL + "/" + this.githubClientRegistration.getClientAlias();
|
||||||
|
|
||||||
|
String authorizationResponseUri =
|
||||||
|
UriComponentsBuilder.fromHttpUrl(redirectUri)
|
||||||
|
.queryParam(OAuth2Parameter.ERROR, error)
|
||||||
|
.queryParam(OAuth2Parameter.STATE, state)
|
||||||
|
.build().encode().toUriString();
|
||||||
|
|
||||||
|
page = this.webClient.getPage(new URL(authorizationResponseUri));
|
||||||
|
assertThat(page.getBaseURL()).isEqualTo(loginErrorPageUrl);
|
||||||
|
|
||||||
|
HtmlElement errorElement = page.getBody().getFirstByXPath("p");
|
||||||
|
assertThat(errorElement).isNotNull();
|
||||||
|
assertThat(errorElement.asText()).contains(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertLoginPage(HtmlPage page) throws Exception {
|
||||||
|
assertThat(page.getTitleText()).isEqualTo("Login Page");
|
||||||
|
|
||||||
|
int expectedClients = 4;
|
||||||
|
|
||||||
|
List<HtmlAnchor> clientAnchorElements = page.getAnchors();
|
||||||
|
assertThat(clientAnchorElements.size()).isEqualTo(expectedClients);
|
||||||
|
|
||||||
|
String baseAuthorizeUri = AUTHORIZATION_BASE_URI + "/";
|
||||||
|
String googleClientAuthorizeUri = baseAuthorizeUri + this.googleClientRegistration.getClientAlias();
|
||||||
|
String githubClientAuthorizeUri = baseAuthorizeUri + this.githubClientRegistration.getClientAlias();
|
||||||
|
String facebookClientAuthorizeUri = baseAuthorizeUri + this.facebookClientRegistration.getClientAlias();
|
||||||
|
String oktaClientAuthorizeUri = baseAuthorizeUri + this.oktaClientRegistration.getClientAlias();
|
||||||
|
|
||||||
|
for (int i=0; i<expectedClients; i++) {
|
||||||
|
assertThat(clientAnchorElements.get(i).getAttribute("href")).isIn(
|
||||||
|
googleClientAuthorizeUri, githubClientAuthorizeUri,
|
||||||
|
facebookClientAuthorizeUri, oktaClientAuthorizeUri);
|
||||||
|
assertThat(clientAnchorElements.get(i).asText()).isIn(
|
||||||
|
this.googleClientRegistration.getClientName(),
|
||||||
|
this.githubClientRegistration.getClientName(),
|
||||||
|
this.facebookClientRegistration.getClientName(),
|
||||||
|
this.oktaClientRegistration.getClientName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertUserInfoPage(HtmlPage page) throws Exception {
|
||||||
|
assertThat(page.getTitleText()).isEqualTo("Spring Security - OAuth2 User Info");
|
||||||
|
|
||||||
|
DomNodeList<HtmlElement> divElements = page.getBody().getElementsByTagName("div");
|
||||||
|
assertThat(divElements.get(1).asText()).contains("User: joeg@springsecurity.io");
|
||||||
|
assertThat(divElements.get(4).asText()).contains("Name: joeg@springsecurity.io");
|
||||||
|
}
|
||||||
|
|
||||||
|
private HtmlAnchor getClientAnchorElement(HtmlPage page, ClientRegistration clientRegistration) {
|
||||||
|
Optional<HtmlAnchor> clientAnchorElement = page.getAnchors().stream()
|
||||||
|
.filter(e -> e.asText().equals(clientRegistration.getClientName())).findFirst();
|
||||||
|
|
||||||
|
return (clientAnchorElement.isPresent() ? clientAnchorElement.get() : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebResponse followLinkDisableRedirects(HtmlAnchor anchorElement) throws Exception {
|
||||||
|
WebResponse response = null;
|
||||||
|
try {
|
||||||
|
// Disable the automatic redirection (which will trigger
|
||||||
|
// an exception) so that we can capture the response
|
||||||
|
this.webClient.getOptions().setRedirectEnabled(false);
|
||||||
|
anchorElement.click();
|
||||||
|
} catch (FailingHttpStatusCodeException ex) {
|
||||||
|
response = ex.getResponse();
|
||||||
|
this.webClient.getOptions().setRedirectEnabled(true);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EnableWebSecurity
|
||||||
|
public static class SecurityTestConfig extends WebSecurityConfigurerAdapter {
|
||||||
|
|
||||||
|
// @formatter:off
|
||||||
|
@Override
|
||||||
|
protected void configure(HttpSecurity http) throws Exception {
|
||||||
|
http
|
||||||
|
.authorizeRequests()
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
.and()
|
||||||
|
.oauth2Login()
|
||||||
|
.authorizationCodeTokenExchanger(this.mockAuthorizationCodeTokenExchanger())
|
||||||
|
.userInfoEndpoint()
|
||||||
|
.userInfoService(this.mockUserInfoService());
|
||||||
|
}
|
||||||
|
// @formatter:on
|
||||||
|
|
||||||
|
private AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> mockAuthorizationCodeTokenExchanger() {
|
||||||
|
TokenResponseAttributes tokenResponse = TokenResponseAttributes.withToken("access-token-1234")
|
||||||
|
.tokenType(AccessToken.TokenType.BEARER)
|
||||||
|
.expiresIn(60 * 1000)
|
||||||
|
.scopes(Collections.singleton("openid"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
AuthorizationGrantTokenExchanger mock = mock(AuthorizationGrantTokenExchanger.class);
|
||||||
|
when(mock.exchange(any())).thenReturn(tokenResponse);
|
||||||
|
return mock;
|
||||||
|
}
|
||||||
|
|
||||||
|
private OAuth2UserService mockUserInfoService() {
|
||||||
|
Map<String, Object> attributes = new HashMap<>();
|
||||||
|
attributes.put("id", "joeg");
|
||||||
|
attributes.put("first-name", "Joe");
|
||||||
|
attributes.put("last-name", "Grandja");
|
||||||
|
attributes.put("email", "joeg@springsecurity.io");
|
||||||
|
|
||||||
|
DefaultOAuth2User user = new DefaultOAuth2User(attributes, "email");
|
||||||
|
|
||||||
|
OAuth2UserService mock = mock(OAuth2UserService.class);
|
||||||
|
when(mock.loadUser(any())).thenReturn(user);
|
||||||
|
return mock;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SpringBootConfiguration
|
||||||
|
@EnableAutoConfiguration
|
||||||
|
@ComponentScan(basePackages = "org.springframework.security.samples.web")
|
||||||
|
public static class SpringBootApplicationTestConfig {
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,129 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.boot.autoconfigure.security.oauth2.client;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.*;
|
||||||
|
import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration;
|
||||||
|
import org.springframework.boot.bind.PropertySourcesBinder;
|
||||||
|
import org.springframework.boot.bind.RelaxedPropertyResolver;
|
||||||
|
import org.springframework.context.annotation.*;
|
||||||
|
import org.springframework.core.env.ConfigurableEnvironment;
|
||||||
|
import org.springframework.core.env.Environment;
|
||||||
|
import org.springframework.core.env.MutablePropertySources;
|
||||||
|
import org.springframework.core.env.PropertiesPropertySource;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.core.type.AnnotatedTypeMetadata;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationProperties;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||||
|
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Joe Grandja
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@ConditionalOnWebApplication
|
||||||
|
@ConditionalOnClass(ClientRegistrationRepository.class)
|
||||||
|
@ConditionalOnMissingBean(ClientRegistrationRepository.class)
|
||||||
|
@AutoConfigureBefore(SecurityAutoConfiguration.class)
|
||||||
|
public class ClientRegistrationAutoConfiguration {
|
||||||
|
private static final String CLIENT_ID_PROPERTY = "client-id";
|
||||||
|
private static final String CLIENTS_DEFAULTS_RESOURCE = "META-INF/oauth2-clients-defaults.yml";
|
||||||
|
static final String CLIENT_PROPERTY_PREFIX = "security.oauth2.client.";
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@Conditional(ClientPropertiesAvailableCondition.class)
|
||||||
|
protected static class ClientRegistrationConfiguration {
|
||||||
|
private final Environment environment;
|
||||||
|
|
||||||
|
protected ClientRegistrationConfiguration(Environment environment) {
|
||||||
|
this.environment = environment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ClientRegistrationRepository clientRegistrationRepository() {
|
||||||
|
MutablePropertySources propertySources = ((ConfigurableEnvironment) this.environment).getPropertySources();
|
||||||
|
Properties clientsDefaultProperties = this.getClientsDefaultProperties();
|
||||||
|
if (clientsDefaultProperties != null) {
|
||||||
|
propertySources.addLast(new PropertiesPropertySource("oauth2ClientsDefaults", clientsDefaultProperties));
|
||||||
|
}
|
||||||
|
PropertySourcesBinder binder = new PropertySourcesBinder(propertySources);
|
||||||
|
RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(this.environment, CLIENT_PROPERTY_PREFIX);
|
||||||
|
|
||||||
|
List<ClientRegistration> clientRegistrations = new ArrayList<>();
|
||||||
|
|
||||||
|
Set<String> clientPropertyKeys = resolveClientPropertyKeys(this.environment);
|
||||||
|
for (String clientPropertyKey : clientPropertyKeys) {
|
||||||
|
if (!resolver.containsProperty(clientPropertyKey + "." + CLIENT_ID_PROPERTY)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ClientRegistrationProperties clientRegistrationProperties = new ClientRegistrationProperties();
|
||||||
|
binder.bindTo(CLIENT_PROPERTY_PREFIX + clientPropertyKey, clientRegistrationProperties);
|
||||||
|
ClientRegistration clientRegistration = new ClientRegistration.Builder(clientRegistrationProperties).build();
|
||||||
|
clientRegistrations.add(clientRegistration);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new InMemoryClientRegistrationRepository(clientRegistrations);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Properties getClientsDefaultProperties() {
|
||||||
|
ClassPathResource clientsDefaultsResource = new ClassPathResource(CLIENTS_DEFAULTS_RESOURCE);
|
||||||
|
if (!clientsDefaultsResource.exists()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
YamlPropertiesFactoryBean yamlPropertiesFactory = new YamlPropertiesFactoryBean();
|
||||||
|
yamlPropertiesFactory.setResources(clientsDefaultsResource);
|
||||||
|
return yamlPropertiesFactory.getObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Set<String> resolveClientPropertyKeys(Environment environment) {
|
||||||
|
Set<String> clientPropertyKeys = new LinkedHashSet<>();
|
||||||
|
RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(environment, CLIENT_PROPERTY_PREFIX);
|
||||||
|
resolver.getSubProperties("").keySet().forEach(key -> {
|
||||||
|
int endIndex = key.indexOf('.');
|
||||||
|
if (endIndex != -1) {
|
||||||
|
clientPropertyKeys.add(key.substring(0, endIndex));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return clientPropertyKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ClientPropertiesAvailableCondition extends SpringBootCondition implements ConfigurationCondition {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ConfigurationCondition.ConfigurationPhase getConfigurationPhase() {
|
||||||
|
return ConfigurationPhase.PARSE_CONFIGURATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
|
||||||
|
ConditionMessage.Builder message = ConditionMessage.forCondition("OAuth2 Client Properties");
|
||||||
|
Set<String> clientPropertyKeys = resolveClientPropertyKeys(context.getEnvironment());
|
||||||
|
if (!CollectionUtils.isEmpty(clientPropertyKeys)) {
|
||||||
|
return ConditionOutcome.match(message.foundExactly("OAuth2 Client(s) -> " +
|
||||||
|
clientPropertyKeys.stream().collect(Collectors.joining(", "))));
|
||||||
|
}
|
||||||
|
return ConditionOutcome.noMatch(message.notAvailable("OAuth2 Client(s)"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.boot.autoconfigure.security.oauth2.client;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.convert.converter.Converter;
|
||||||
|
import org.springframework.core.env.Environment;
|
||||||
|
import org.springframework.http.client.ClientHttpResponse;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||||
|
import org.springframework.security.oauth2.client.user.converter.AbstractOAuth2UserConverter;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
|
import org.springframework.util.ClassUtils;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.springframework.boot.autoconfigure.security.oauth2.client.ClientRegistrationAutoConfiguration.CLIENT_PROPERTY_PREFIX;
|
||||||
|
import static org.springframework.boot.autoconfigure.security.oauth2.client.ClientRegistrationAutoConfiguration.resolveClientPropertyKeys;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Joe Grandja
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@ConditionalOnWebApplication
|
||||||
|
@ConditionalOnClass(EnableWebSecurity.class)
|
||||||
|
@ConditionalOnMissingBean(WebSecurityConfiguration.class)
|
||||||
|
@ConditionalOnBean(ClientRegistrationRepository.class)
|
||||||
|
@AutoConfigureBefore(SecurityAutoConfiguration.class)
|
||||||
|
@AutoConfigureAfter(ClientRegistrationAutoConfiguration.class)
|
||||||
|
public class OAuth2LoginAutoConfiguration {
|
||||||
|
private static final String USER_INFO_URI_PROPERTY = "user-info-uri";
|
||||||
|
private static final String USER_INFO_CONVERTER_PROPERTY = "user-info-converter";
|
||||||
|
private static final String USER_INFO_NAME_ATTR_KEY_PROPERTY = "user-info-name-attribute-key";
|
||||||
|
|
||||||
|
@EnableWebSecurity
|
||||||
|
protected static class OAuth2LoginSecurityConfiguration extends WebSecurityConfigurerAdapter {
|
||||||
|
private final Environment environment;
|
||||||
|
|
||||||
|
protected OAuth2LoginSecurityConfiguration(Environment environment) {
|
||||||
|
this.environment = environment;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @formatter:off
|
||||||
|
@Override
|
||||||
|
protected void configure(HttpSecurity http) throws Exception {
|
||||||
|
http
|
||||||
|
.authorizeRequests()
|
||||||
|
.antMatchers("/favicon.ico").permitAll()
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
.and()
|
||||||
|
.oauth2Login();
|
||||||
|
|
||||||
|
this.registerUserInfoTypeConverters(http.oauth2Login());
|
||||||
|
}
|
||||||
|
// @formatter:on
|
||||||
|
|
||||||
|
private void registerUserInfoTypeConverters(OAuth2LoginConfigurer<HttpSecurity> oauth2LoginConfigurer) throws Exception {
|
||||||
|
Set<String> clientPropertyKeys = resolveClientPropertyKeys(this.environment);
|
||||||
|
for (String clientPropertyKey : clientPropertyKeys) {
|
||||||
|
String fullClientPropertyKey = CLIENT_PROPERTY_PREFIX + clientPropertyKey + ".";
|
||||||
|
String userInfoUriValue = this.environment.getProperty(fullClientPropertyKey + USER_INFO_URI_PROPERTY);
|
||||||
|
String userInfoConverterTypeValue = this.environment.getProperty(fullClientPropertyKey + USER_INFO_CONVERTER_PROPERTY);
|
||||||
|
if (userInfoUriValue != null && userInfoConverterTypeValue != null) {
|
||||||
|
Class<? extends Converter> userInfoConverterType = ClassUtils.resolveClassName(
|
||||||
|
userInfoConverterTypeValue, this.getClass().getClassLoader()).asSubclass(Converter.class);
|
||||||
|
Converter<ClientHttpResponse, ? extends OAuth2User> userInfoConverter = null;
|
||||||
|
if (AbstractOAuth2UserConverter.class.isAssignableFrom(userInfoConverterType)) {
|
||||||
|
Constructor<? extends Converter> oauth2UserConverterConstructor = ClassUtils.getConstructorIfAvailable(userInfoConverterType, String.class);
|
||||||
|
if (oauth2UserConverterConstructor != null) {
|
||||||
|
String userInfoNameAttributeKey = this.environment.getProperty(fullClientPropertyKey + USER_INFO_NAME_ATTR_KEY_PROPERTY);
|
||||||
|
userInfoConverter = (Converter<ClientHttpResponse, ? extends OAuth2User>)oauth2UserConverterConstructor.newInstance(userInfoNameAttributeKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (userInfoConverter == null) {
|
||||||
|
userInfoConverter = (Converter<ClientHttpResponse, ? extends OAuth2User>)userInfoConverterType.newInstance();
|
||||||
|
}
|
||||||
|
oauth2LoginConfigurer.userInfoEndpoint().userInfoTypeConverter(userInfoConverter, new URI(userInfoUriValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.security.samples.user;
|
||||||
|
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Joe Grandja
|
||||||
|
*/
|
||||||
|
public class GitHubOAuth2User implements OAuth2User {
|
||||||
|
private String id;
|
||||||
|
private String name;
|
||||||
|
private String login;
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
public GitHubOAuth2User() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getAttributes() {
|
||||||
|
Map<String, Object> attributes = new HashMap<>();
|
||||||
|
attributes.put("id", this.getId());
|
||||||
|
attributes.put("name", this.getName());
|
||||||
|
attributes.put("login", this.getLogin());
|
||||||
|
attributes.put("email", this.getEmail());
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLogin() {
|
||||||
|
return this.login;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLogin(String login) {
|
||||||
|
this.login = login;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmail() {
|
||||||
|
return this.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEmail(String email) {
|
||||||
|
this.email = email;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,33 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
|
||||||
|
<head>
|
||||||
|
<title>Spring Security - OAuth2 User Info</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div style="float: right" th:fragment="logout" sec:authorize="isAuthenticated()">
|
||||||
|
<div style="float:left">
|
||||||
|
<span style="font-weight:bold">User: </span><span sec:authentication="name"></span>
|
||||||
|
</div>
|
||||||
|
<div style="float:none"> </div>
|
||||||
|
<div style="float:right">
|
||||||
|
<form action="#" th:action="@{/logout}" method="post">
|
||||||
|
<input type="submit" value="Logout" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h1>OAuth2 User Info</h1>
|
||||||
|
<div>
|
||||||
|
<span style="font-weight:bold">Name: </span><span th:text="${userName}"></span>
|
||||||
|
</div>
|
||||||
|
<div> </div>
|
||||||
|
<div>
|
||||||
|
<span style="font-weight:bold">Attributes:</span>
|
||||||
|
<ul>
|
||||||
|
<li th:each="userAttribute : ${userAttributes}">
|
||||||
|
<span style="font-weight:bold" th:text="${userAttribute.key}"></span>: <span th:text="${userAttribute.value}"></span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -15,7 +15,13 @@
|
||||||
*/
|
*/
|
||||||
package org.springframework.security.web.authentication.ui;
|
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.FilterChain;
|
||||||
import javax.servlet.ServletException;
|
import javax.servlet.ServletException;
|
||||||
|
@ -24,14 +30,8 @@ import javax.servlet.ServletResponse;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import javax.servlet.http.HttpSession;
|
import javax.servlet.http.HttpSession;
|
||||||
|
import java.io.IOException;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
import java.util.Map;
|
||||||
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;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For internal use with namespace configuration in the case where a user doesn't
|
* 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 String failureUrl;
|
||||||
private boolean formLoginEnabled;
|
private boolean formLoginEnabled;
|
||||||
private boolean openIdEnabled;
|
private boolean openIdEnabled;
|
||||||
|
private boolean oauth2LoginEnabled;
|
||||||
private String authenticationUrl;
|
private String authenticationUrl;
|
||||||
private String usernameParameter;
|
private String usernameParameter;
|
||||||
private String passwordParameter;
|
private String passwordParameter;
|
||||||
|
@ -58,6 +59,8 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
||||||
private String openIDauthenticationUrl;
|
private String openIDauthenticationUrl;
|
||||||
private String openIDusernameParameter;
|
private String openIDusernameParameter;
|
||||||
private String openIDrememberMeParameter;
|
private String openIDrememberMeParameter;
|
||||||
|
private Map<String, String> oauth2AuthenticationUrlToClientName;
|
||||||
|
|
||||||
|
|
||||||
public DefaultLoginPageGeneratingFilter() {
|
public DefaultLoginPageGeneratingFilter() {
|
||||||
}
|
}
|
||||||
|
@ -105,7 +108,7 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
return formLoginEnabled || openIdEnabled;
|
return formLoginEnabled || openIdEnabled || oauth2LoginEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLogoutSuccessUrl(String logoutSuccessUrl) {
|
public void setLogoutSuccessUrl(String logoutSuccessUrl) {
|
||||||
|
@ -132,6 +135,10 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
||||||
this.openIdEnabled = openIdEnabled;
|
this.openIdEnabled = openIdEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setOauth2LoginEnabled(boolean oauth2LoginEnabled) {
|
||||||
|
this.oauth2LoginEnabled = oauth2LoginEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
public void setAuthenticationUrl(String authenticationUrl) {
|
public void setAuthenticationUrl(String authenticationUrl) {
|
||||||
this.authenticationUrl = authenticationUrl;
|
this.authenticationUrl = authenticationUrl;
|
||||||
}
|
}
|
||||||
|
@ -157,6 +164,10 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
||||||
this.openIDusernameParameter = openIDusernameParameter;
|
this.openIDusernameParameter = openIDusernameParameter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setOauth2AuthenticationUrlToClientName(Map<String, String> oauth2AuthenticationUrlToClientName) {
|
||||||
|
this.oauth2AuthenticationUrlToClientName = oauth2AuthenticationUrlToClientName;
|
||||||
|
}
|
||||||
|
|
||||||
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
|
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
|
||||||
throws IOException, ServletException {
|
throws IOException, ServletException {
|
||||||
HttpServletRequest request = (HttpServletRequest) req;
|
HttpServletRequest request = (HttpServletRequest) req;
|
||||||
|
@ -201,13 +212,13 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loginError) {
|
if (loginError) {
|
||||||
sb.append("<p><font color='red'>Your login attempt was not successful, try again.<br/><br/>Reason: ");
|
sb.append("<p style='color:red;'>Your login attempt was not successful, try again.<br/><br/>Reason: ");
|
||||||
sb.append(errorMsg);
|
sb.append(errorMsg);
|
||||||
sb.append("</font></p>");
|
sb.append("</p>");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (logoutSuccess) {
|
if (logoutSuccess) {
|
||||||
sb.append("<p><font color='green'>You have been logged out</font></p>");
|
sb.append("<p style='color:green;'>You have been logged out</p>");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (formLoginEnabled) {
|
if (formLoginEnabled) {
|
||||||
|
@ -252,6 +263,19 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
||||||
sb.append("</form>");
|
sb.append("</form>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (oauth2LoginEnabled) {
|
||||||
|
sb.append("<h3>Login with OAuth 2.0</h3>");
|
||||||
|
sb.append("<table>\n");
|
||||||
|
for (Map.Entry<String, String> clientAuthenticationUrlToClientName : oauth2AuthenticationUrlToClientName.entrySet()) {
|
||||||
|
sb.append(" <tr><td>");
|
||||||
|
sb.append("<a href=\"").append(clientAuthenticationUrlToClientName.getKey()).append("\">");
|
||||||
|
sb.append(clientAuthenticationUrlToClientName.getValue());
|
||||||
|
sb.append("</a>");
|
||||||
|
sb.append("</td></tr>\n");
|
||||||
|
}
|
||||||
|
sb.append("</table>\n");
|
||||||
|
}
|
||||||
|
|
||||||
sb.append("</body></html>");
|
sb.append("</body></html>");
|
||||||
|
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
|
|
Loading…
Reference in New Issue