package mujina.sp;
import mujina.saml.KeyStoreLocator;
import mujina.saml.ProxiedSAMLContextProviderLB;
import org.apache.velocity.app.VelocityEngine;
import org.opensaml.saml2.metadata.provider.MetadataProvider;
import org.opensaml.saml2.metadata.provider.MetadataProviderException;
import org.opensaml.xml.parse.ParserPool;
import org.opensaml.xml.parse.StaticBasicParserPool;
import org.opensaml.xml.parse.XMLParserException;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.saml.SAMLAuthenticationProvider;
import org.springframework.security.saml.SAMLEntryPoint;
import org.springframework.security.saml.SAMLProcessingFilter;
import org.springframework.security.saml.context.SAMLContextProvider;
import org.springframework.security.saml.context.SAMLContextProviderImpl;
import org.springframework.security.saml.key.JKSKeyManager;
import org.springframework.security.saml.metadata.CachingMetadataManager;
import org.springframework.security.saml.metadata.ExtendedMetadata;
import org.springframework.security.saml.metadata.ExtendedMetadataDelegate;
import org.springframework.security.saml.metadata.MetadataDisplayFilter;
import org.springframework.security.saml.metadata.MetadataGenerator;
import org.springframework.security.saml.metadata.MetadataGeneratorFilter;
import org.springframework.security.saml.parser.ParserPoolHolder;
import org.springframework.security.saml.util.VelocityFactory;
import org.springframework.security.saml.websso.WebSSOProfileOptions;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.Filter;
import javax.xml.stream.XMLStreamException;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Value("${sp.idp_metadata_url}")
private String identityProviderMetadataUrl;
@Value("${sp.base_url}")
private String spBaseUrl;
@Value("${sp.entity_id}")
private String spEntityId;
@Value("${sp.private_key}")
private String spPrivateKey;
@Value("${sp.certificate}")
private String spCertificate;
@Value("${sp.passphrase}")
private String spPassphrase;
@Value("${sp.acs_location_path}")
private String assertionConsumerServiceURLPath;
private DefaultResourceLoader defaultResourceLoader = new DefaultResourceLoader();
@Bean
public SAMLAuthenticationProvider samlAuthenticationProvider() {
SAMLAuthenticationProvider samlAuthenticationProvider = new RoleSAMLAuthenticationProvider();
samlAuthenticationProvider.setUserDetails(new DefaultSAMLUserDetailsService());
samlAuthenticationProvider.setForcePrincipalAsString(false);
samlAuthenticationProvider.setExcludeCredential(true);
return samlAuthenticationProvider;
}
@Bean
public SAMLEntryPoint samlEntryPoint() {
WebSSOProfileOptions webSSOProfileOptions = new WebSSOProfileOptions();
webSSOProfileOptions.setIncludeScoping(false);
SAMLEntryPoint samlEntryPoint = new SAMLEntryPoint();
samlEntryPoint.setFilterProcessesUrl("login");
samlEntryPoint.setDefaultProfileOptions(webSSOProfileOptions);
return samlEntryPoint;
}
@Bean
public ServletContextInitializer servletContextInitializer() {
//otherwise the two localhost instances override each other session
return servletContext -> servletContext.getSessionCookieConfig().setName("mujinaSpSessionId");
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/health", "/info");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/metadata", "/favicon.ico", "/css.*", "/api/**", assertionConsumerServiceURLPath + "/**").permitAll()
.anyRequest().hasRole("USER")
.and()
.httpBasic().authenticationEntryPoint(samlEntryPoint())
.and()
.csrf().disable()
.addFilterBefore(metadataGeneratorFilter(), ChannelProcessingFilter.class)
.addFilterAfter(samlFilter(), BasicAuthenticationFilter.class)
.logout()
.logoutSuccessUrl("/");
}
// Handler deciding where to redirect user after successful login
@Bean
public SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler() {
SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler =
new SavedRequestAwareAuthenticationSuccessHandler();
successRedirectHandler.setDefaultTargetUrl("/user");
return successRedirectHandler;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(samlAuthenticationProvider());
}
@Bean
public MetadataDisplayFilter metadataDisplayFilter() {
DefaultMetadataDisplayFilter displayFilter = new DefaultMetadataDisplayFilter();
displayFilter.setFilterProcessesUrl("metadata");
return displayFilter;
}
@Bean
public SimpleUrlAuthenticationFailureHandler authenticationFailureHandler() {
SimpleUrlAuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
failureHandler.setUseForward(true);
failureHandler.setDefaultFailureUrl("/error");
return failureHandler;
}
@Bean
public SAMLProcessingFilter samlWebSSOProcessingFilter() throws Exception {
SAMLProcessingFilter samlWebSSOProcessingFilter = new SAMLProcessingFilter();
samlWebSSOProcessingFilter.setFilterProcessesUrl("saml/SSO");
samlWebSSOProcessingFilter.setAuthenticationManager(authenticationManager());
samlWebSSOProcessingFilter.setAuthenticationSuccessHandler(successRedirectHandler());
samlWebSSOProcessingFilter.setAuthenticationFailureHandler(authenticationFailureHandler());
return samlWebSSOProcessingFilter;
}
@Bean
public MetadataGeneratorFilter metadataGeneratorFilter() throws InvalidKeySpecException, CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, XMLStreamException {
return new MetadataGeneratorFilter(metadataGenerator());
}
@Bean
public FilterChainProxy samlFilter() throws Exception {
List<SecurityFilterChain> chains = new ArrayList<>();
chains.add(chain("/login/**", samlEntryPoint()));
chains.add(chain("/metadata/**", metadataDisplayFilter()));
chains.add(chain(assertionConsumerServiceURLPath + "/**", samlWebSSOProcessingFilter()));
return new FilterChainProxy(chains);
}
private DefaultSecurityFilterChain chain(String pattern, Filter entryPoint) {
return new DefaultSecurityFilterChain(new AntPathRequestMatcher(pattern), entryPoint);
}
@Bean
public ExtendedMetadata extendedMetadata() {
ExtendedMetadata extendedMetadata = new ExtendedMetadata();
extendedMetadata.setIdpDiscoveryEnabled(false);
extendedMetadata.setSignMetadata(true);
return extendedMetadata;
}
@Bean
public MetadataProvider identityProvider() throws MetadataProviderException, XMLParserException {
Resource resource = defaultResourceLoader.getResource(identityProviderMetadataUrl);
ResourceMetadataProvider resourceMetadataProvider = new ResourceMetadataProvider(resource);
resourceMetadataProvider.setParserPool(parserPool());
ExtendedMetadataDelegate extendedMetadataDelegate = new ExtendedMetadataDelegate(resourceMetadataProvider, extendedMetadata());
extendedMetadataDelegate.setMetadataTrustCheck(true);
extendedMetadataDelegate.setMetadataRequireSignature(true);
return extendedMetadataDelegate;
}
@Bean
@Qualifier("metadata")
public CachingMetadataManager metadata() throws MetadataProviderException, XMLParserException {
List<MetadataProvider> providers = new ArrayList<>();
providers.add(identityProvider());
return new CachingMetadataManager(providers);
}
@Bean
public VelocityEngine velocityEngine() {
return VelocityFactory.getEngine();
}
@Bean(initMethod = "initialize")
public ParserPool parserPool() {
return new StaticBasicParserPool();
}
@Bean(name = "parserPoolHolder")
public ParserPoolHolder parserPoolHolder() {
return new ParserPoolHolder();
}
@Bean
public SAMLContextProvider contextProvider() throws URISyntaxException {
return new ProxiedSAMLContextProviderLB(new URI(spBaseUrl));
}
@Bean
public MetadataGenerator metadataGenerator() throws NoSuchAlgorithmException, CertificateException, InvalidKeySpecException, KeyStoreException, IOException, XMLStreamException {
MetadataGenerator metadataGenerator = new MetadataGenerator();
metadataGenerator.setEntityId(spEntityId);
metadataGenerator.setEntityBaseURL(spBaseUrl);
metadataGenerator.setExtendedMetadata(extendedMetadata());
metadataGenerator.setIncludeDiscoveryExtension(false);
metadataGenerator.setKeyManager(keyManager());
return metadataGenerator;
}
@Bean
public JKSKeyManager keyManager() throws InvalidKeySpecException, CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, XMLStreamException {
KeyStore keyStore = KeyStoreLocator.createKeyStore(spPassphrase);
KeyStoreLocator.addPrivateKey(keyStore, spEntityId, spPrivateKey, spCertificate, spPassphrase);
return new JKSKeyManager(keyStore, Collections.singletonMap(spEntityId, spPassphrase), spEntityId);
}
}