Merge pull request #7260 from fhanik/feature/saml2-sp-mvp

Add SAML Service Provider Support
This commit is contained in:
Filip Hanik
2019-09-05 17:04:14 -07:00
committed by GitHub
39 changed files with 4260 additions and 19 deletions

View File

@@ -73,6 +73,9 @@ final class FilterComparator implements Comparator<Filter>, Serializable {
filterToOrder.put(
"org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter",
order.next());
filterToOrder.put(
"org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter",
order.next());
put(X509AuthenticationFilter.class, order.next());
put(AbstractPreAuthenticatedProcessingFilter.class, order.next());
filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter",
@@ -80,6 +83,9 @@ final class FilterComparator implements Comparator<Filter>, Serializable {
filterToOrder.put(
"org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter",
order.next());
filterToOrder.put(
"org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter",
order.next());
put(UsernamePasswordAuthenticationFilter.class, order.next());
put(ConcurrentSessionFilter.class, order.next());
filterToOrder.put(

View File

@@ -53,10 +53,13 @@ import org.springframework.security.config.annotation.web.configurers.oauth2.cli
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.config.annotation.web.configurers.openid.OpenIDLoginConfigurer;
import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LoginConfigurer;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.PortMapper;
import org.springframework.security.web.PortMapperImpl;
@@ -75,11 +78,11 @@ import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
import javax.servlet.Filter;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.servlet.Filter;
import javax.servlet.http.HttpServletRequest;
/**
* A {@link HttpSecurity} is similar to Spring Security's XML &lt;http&gt; element in the
@@ -1857,6 +1860,191 @@ public final class HttpSecurity extends
return HttpSecurity.this;
}
/**
* Configures authentication support using an SAML 2.0 Service Provider.
* <br>
* <br>
*
* The &quot;authentication flow&quot; is implemented using the <b>Web Browser SSO Profile, using POST and REDIRECT bindings</b>,
* as documented in the <a target="_blank" href="https://docs.oasis-open.org/security/saml/">SAML V2.0 Core,Profiles and Bindings</a>
* specifications.
* <br>
* <br>
*
* As a prerequisite to using this feature, is that you have a SAML v2.0 Identity Provider to provide an assertion.
* The representation of the Service Provider, the relying party, and the remote Identity Provider, the asserting party
* is contained within {@link RelyingPartyRegistration}.
* <br>
* <br>
*
* {@link RelyingPartyRegistration}(s) are composed within a
* {@link RelyingPartyRegistrationRepository},
* which is <b>required</b> and must be registered with the {@link ApplicationContext} or
* configured via <code>saml2Login().relyingPartyRegistrationRepository(..)</code>.
* <br>
* <br>
*
* The default configuration provides an auto-generated login page at <code>&quot;/login&quot;</code> and
* redirects to <code>&quot;/login?error&quot;</code> when an authentication error occurs.
* The login page will display each of the identity providers with a link
* that is capable of initiating the &quot;authentication flow&quot;.
* <br>
* <br>
*
* <p>
* <h2>Example Configuration</h2>
*
* The following example shows the minimal configuration required, using SimpleSamlPhp as the Authentication Provider.
*
* <pre>
* &#064;Configuration
* public class Saml2LoginConfig {
*
* &#064;EnableWebSecurity
* public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
* &#064;Override
* protected void configure(HttpSecurity http) throws Exception {
* http
* .authorizeRequests()
* .anyRequest().authenticated()
* .and()
* .saml2Login();
* }
* }
*
* &#064;Bean
* public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
* return new InMemoryRelyingPartyRegistrationRepository(this.getSaml2RelyingPartyRegistration());
* }
*
* private RelyingPartyRegistration getSaml2RelyingPartyRegistration() {
* //remote IDP entity ID
* String idpEntityId = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php";
* //remote WebSSO Endpoint - Where to Send AuthNRequests to
* String webSsoEndpoint = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php";
* //local registration ID
* String registrationId = "simplesamlphp";
* //local entity ID - autogenerated based on URL
* String localEntityIdTemplate = "{baseUrl}/saml2/service-provider-metadata/{registrationId}";
* //local signing (and decryption key)
* Saml2X509Credential signingCredential = getSigningCredential();
* //IDP certificate for verification of incoming messages
* Saml2X509Credential idpVerificationCertificate = getVerificationCertificate();
* return RelyingPartyRegistration.withRegistrationId(registrationId)
* * .remoteIdpEntityId(idpEntityId)
* * .idpWebSsoUrl(webSsoEndpoint)
* * .credential(signingCredential)
* * .credential(idpVerificationCertificate)
* * .localEntityIdTemplate(localEntityIdTemplate)
* * .build();
* }
* }
* </pre>
*
* <p>
*
* @since 5.2
* @return the {@link Saml2LoginConfigurer} for further customizations
* @throws Exception
*/
public Saml2LoginConfigurer<HttpSecurity> saml2Login() throws Exception {
return getOrApply(new Saml2LoginConfigurer<>());
}
/**
* Configures authentication support using an SAML 2.0 Service Provider.
* <br>
* <br>
*
* The &quot;authentication flow&quot; is implemented using the <b>Web Browser SSO Profile, using POST and REDIRECT bindings</b>,
* as documented in the <a target="_blank" href="https://docs.oasis-open.org/security/saml/">SAML V2.0 Core,Profiles and Bindings</a>
* specifications.
* <br>
* <br>
*
* As a prerequisite to using this feature, is that you have a SAML v2.0 Identity Provider to provide an assertion.
* The representation of the Service Provider, the relying party, and the remote Identity Provider, the asserting party
* is contained within {@link RelyingPartyRegistration}.
* <br>
* <br>
*
* {@link RelyingPartyRegistration}(s) are composed within a
* {@link RelyingPartyRegistrationRepository},
* which is <b>required</b> and must be registered with the {@link ApplicationContext} or
* configured via <code>saml2Login().relyingPartyRegistrationRepository(..)</code>.
* <br>
* <br>
*
* The default configuration provides an auto-generated login page at <code>&quot;/login&quot;</code> and
* redirects to <code>&quot;/login?error&quot;</code> when an authentication error occurs.
* The login page will display each of the identity providers with a link
* that is capable of initiating the &quot;authentication flow&quot;.
* <br>
* <br>
*
* <p>
* <h2>Example Configuration</h2>
*
* The following example shows the minimal configuration required, using SimpleSamlPhp as the Authentication Provider.
*
* <pre>
* &#064;Configuration
* public class Saml2LoginConfig {
*
* &#064;EnableWebSecurity
* public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
* &#064;Override
* protected void configure(HttpSecurity http) throws Exception {
* http
* .authorizeRequests()
* .anyRequest().authenticated()
* .and()
* .saml2Login(withDefaults());
* }
* }
*
* &#064;Bean
* public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
* return new InMemoryRelyingPartyRegistrationRepository(this.getSaml2RelyingPartyRegistration());
* }
*
* private RelyingPartyRegistration getSaml2RelyingPartyRegistration() {
* //remote IDP entity ID
* String idpEntityId = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php";
* //remote WebSSO Endpoint - Where to Send AuthNRequests to
* String webSsoEndpoint = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php";
* //local registration ID
* String registrationId = "simplesamlphp";
* //local entity ID - autogenerated based on URL
* String localEntityIdTemplate = "{baseUrl}/saml2/service-provider-metadata/{registrationId}";
* //local signing (and decryption key)
* Saml2X509Credential signingCredential = getSigningCredential();
* //IDP certificate for verification of incoming messages
* Saml2X509Credential idpVerificationCertificate = getVerificationCertificate();
* return RelyingPartyRegistration.withRegistrationId(registrationId)
* * .remoteIdpEntityId(idpEntityId)
* * .idpWebSsoUrl(webSsoEndpoint)
* * .credential(signingCredential)
* * .credential(idpVerificationCertificate)
* * .localEntityIdTemplate(localEntityIdTemplate)
* * .build();
* }
* }
* </pre>
*
* <p>
*
* @since 5.2
* @param saml2LoginCustomizer the {@link Customizer} to provide more options for
* the {@link Saml2LoginConfigurer}
* @return the {@link HttpSecurity} for further customizations
* @throws Exception
*/
public HttpSecurity saml2Login(Customizer<Saml2LoginConfigurer<HttpSecurity>> saml2LoginCustomizer) throws Exception {
saml2LoginCustomizer.customize(getOrApply(new Saml2LoginConfigurer<>()));
return HttpSecurity.this;
}
/**
* Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider.
* <br>

View File

@@ -0,0 +1,314 @@
/*
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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.saml2;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider;
import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationRequestFactory;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestFactory;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter;
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.Filter;
import static org.springframework.util.StringUtils.hasText;
/**
* An {@link AbstractHttpConfigurer} for SAML 2.0 Login,
* which leverages the SAML 2.0 Web Browser Single Sign On (WebSSO) Flow.
*
* <p>
* SAML 2.0 Login provides an application with the capability to have users log in
* by using their existing account at an SAML 2.0 Identity Provider.
*
* <p>
* Defaults are provided for all configuration options with the only required configuration
* being {@link #relyingPartyRegistrationRepository(RelyingPartyRegistrationRepository)} .
* Alternatively, a {@link RelyingPartyRegistrationRepository} {@code @Bean} may be registered instead.
*
* <h2>Security Filters</h2>
*
* The following {@code Filter}'s are populated:
*
* <ul>
* <li>{@link Saml2WebSsoAuthenticationFilter}</li>
* <li>{@link Saml2WebSsoAuthenticationRequestFilter}</li>
* </ul>
*
* <h2>Shared Objects Created</h2>
*
* The following shared objects are populated:
*
* <ul>
* <li>{@link RelyingPartyRegistrationRepository} (required)</li>
* <li>{@link Saml2AuthenticationRequestFactory} (optional)</li>
* </ul>
*
* <h2>Shared Objects Used</h2>
*
* The following shared objects are used:
*
* <ul>
* <li>{@link RelyingPartyRegistrationRepository} (required)</li>
* <li>{@link Saml2AuthenticationRequestFactory} (optional)</li>
* <li>{@link DefaultLoginPageGeneratingFilter} - if {@link #loginPage(String)} is not configured
* and {@code DefaultLoginPageGeneratingFilter} is available, than a default login page will be made available</li>
* </ul>
*
* @since 5.2
* @see HttpSecurity#saml2Login()
* @see Saml2WebSsoAuthenticationFilter
* @see Saml2WebSsoAuthenticationRequestFilter
* @see RelyingPartyRegistrationRepository
* @see AbstractAuthenticationFilterConfigurer
*/
public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>> extends
AbstractAuthenticationFilterConfigurer<B, Saml2LoginConfigurer<B>, Saml2WebSsoAuthenticationFilter> {
private String loginPage;
private String loginProcessingUrl = Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI;
private AuthenticationRequestEndpointConfig authenticationRequestEndpoint = new AuthenticationRequestEndpointConfig();
private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
/**
* Sets the {@code RelyingPartyRegistrationRepository} of relying parties, each party representing a
* service provider, SP and this host, and identity provider, IDP pair that communicate with each other.
* @param repo the repository of relying parties
* @return the {@link Saml2LoginConfigurer} for further configuration
*/
public Saml2LoginConfigurer relyingPartyRegistrationRepository(RelyingPartyRegistrationRepository repo) {
this.relyingPartyRegistrationRepository = repo;
return this;
}
/**
* {@inheritDoc}
*/
@Override
public Saml2LoginConfigurer<B> loginPage(String loginPage) {
Assert.hasText(loginPage, "loginPage cannot be empty");
this.loginPage = loginPage;
return this;
}
/**
* {@inheritDoc}
*/
@Override
public Saml2LoginConfigurer<B> loginProcessingUrl(String loginProcessingUrl) {
Assert.hasText(loginProcessingUrl, "loginProcessingUrl cannot be empty");
Assert.state(loginProcessingUrl.contains("{registrationId}"), "{registrationId} path variable is required");
this.loginProcessingUrl = loginProcessingUrl;
return this;
}
/**
* {@inheritDoc}
*/
@Override
protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) {
return new AntPathRequestMatcher(loginProcessingUrl);
}
/**
* {@inheritDoc}
*
* Initializes this filter chain for SAML 2 Login.
* The following actions are taken:
* <ul>
* <li>The WebSSO endpoint has CSRF disabled, typically {@code /login/saml2/sso}</li>
* <li>A {@link Saml2WebSsoAuthenticationFilter is configured}</li>
* <li>The {@code loginProcessingUrl} is set</li>
* <li>A custom login page is configured, <b>or</b></li>
* <li>A default login page with all SAML 2.0 Identity Providers is configured</li>
* <li>An {@link OpenSamlAuthenticationProvider} is configured</li>
* </ul>
*/
@Override
public void init(B http) throws Exception {
registerDefaultCsrfOverride(http);
if (this.relyingPartyRegistrationRepository == null) {
this.relyingPartyRegistrationRepository = getSharedOrBean(http, RelyingPartyRegistrationRepository.class);
}
Saml2WebSsoAuthenticationFilter webSsoFilter = new Saml2WebSsoAuthenticationFilter(this.relyingPartyRegistrationRepository);
setAuthenticationFilter(webSsoFilter);
super.loginProcessingUrl(this.loginProcessingUrl);
if (hasText(this.loginPage)) {
// Set custom login page
super.loginPage(this.loginPage);
super.init(http);
} else {
final Map<String, String> providerUrlMap =
getIdentityProviderUrlMap(
this.authenticationRequestEndpoint.filterProcessingUrl,
this.relyingPartyRegistrationRepository
);
boolean singleProvider = providerUrlMap.size() == 1;
if (singleProvider) {
// Setup auto-redirect to provider login page
// when only 1 IDP is configured
this.updateAuthenticationDefaults();
this.updateAccessDefaults(http);
String loginUrl = providerUrlMap.entrySet().iterator().next().getKey();
final LoginUrlAuthenticationEntryPoint entryPoint = new LoginUrlAuthenticationEntryPoint(loginUrl);
registerAuthenticationEntryPoint(http, entryPoint);
}
else {
super.init(http);
}
}
http.authenticationProvider(getAuthenticationProvider());
this.initDefaultLoginFilter(http);
}
/**
* {@inheritDoc}
*
* During the {@code configure} phase, a {@link Saml2WebSsoAuthenticationRequestFilter}
* is added to handle SAML 2.0 AuthNRequest redirects
*/
@Override
public void configure(B http) throws Exception {
http.addFilter(this.authenticationRequestEndpoint.build(http));
super.configure(http);
}
private AuthenticationProvider getAuthenticationProvider() {
AuthenticationProvider provider = new OpenSamlAuthenticationProvider();
return postProcess(provider);
}
private void registerDefaultCsrfOverride(B http) {
CsrfConfigurer<B> csrf = http.getConfigurer(CsrfConfigurer.class);
if (csrf == null) {
return;
}
csrf.ignoringRequestMatchers(
new AntPathRequestMatcher(loginProcessingUrl)
);
}
private void initDefaultLoginFilter(B http) {
DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http.getSharedObject(DefaultLoginPageGeneratingFilter.class);
if (loginPageGeneratingFilter == null || this.isCustomLoginPage()) {
return;
}
loginPageGeneratingFilter.setSaml2LoginEnabled(true);
loginPageGeneratingFilter.setSaml2AuthenticationUrlToProviderName(
this.getIdentityProviderUrlMap(
this.authenticationRequestEndpoint.filterProcessingUrl,
this.relyingPartyRegistrationRepository
)
);
loginPageGeneratingFilter.setLoginPageUrl(this.getLoginPage());
loginPageGeneratingFilter.setFailureUrl(this.getFailureUrl());
}
@SuppressWarnings("unchecked")
private Map<String, String> getIdentityProviderUrlMap(
String authRequestPrefixUrl,
RelyingPartyRegistrationRepository idpRepo
) {
Map<String, String> idps = new LinkedHashMap<>();
if (idpRepo instanceof Iterable) {
Iterable<RelyingPartyRegistration> repo = (Iterable<RelyingPartyRegistration>) idpRepo;
repo.forEach(
p ->
idps.put(
authRequestPrefixUrl.replace("{registrationId}", p.getRegistrationId()),
p.getRegistrationId()
)
);
}
return idps;
}
private <C> C getSharedOrBean(B http, Class<C> clazz) {
C shared = http.getSharedObject(clazz);
if (shared != null) {
return shared;
}
return getBeanOrNull(http, clazz);
}
private <C> C getBeanOrNull(B http, Class<C> clazz) {
ApplicationContext context = http.getSharedObject(ApplicationContext.class);
if (context == null) {
return null;
}
try {
return context.getBean(clazz);
} catch (NoSuchBeanDefinitionException e) {}
return null;
}
private <C> void setSharedObject(B http, Class<C> clazz, C object) {
if (http.getSharedObject(clazz) == null) {
http.setSharedObject(clazz, object);
}
}
private final class AuthenticationRequestEndpointConfig {
private String filterProcessingUrl = "/saml2/authenticate/{registrationId}";
private AuthenticationRequestEndpointConfig() {
}
private Filter build(B http) {
Saml2AuthenticationRequestFactory authenticationRequestResolver = getResolver(http);
Saml2WebSsoAuthenticationRequestFilter authenticationRequestFilter =
new Saml2WebSsoAuthenticationRequestFilter(Saml2LoginConfigurer.this.relyingPartyRegistrationRepository);
authenticationRequestFilter.setAuthenticationRequestFactory(authenticationRequestResolver);
return authenticationRequestFilter;
}
private Saml2AuthenticationRequestFactory getResolver(B http) {
Saml2AuthenticationRequestFactory resolver = getSharedOrBean(http, Saml2AuthenticationRequestFactory.class);
if (resolver == null ) {
resolver = new OpenSamlAuthenticationRequestFactory();
}
return resolver;
}
}
}