All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.choonchernlim.security.adfs.saml2.SAMLWebSecurityConfigurerAdapter Maven / Gradle / Ivy

There is a newer version: 0.9.0
Show newest version
package com.github.choonchernlim.security.adfs.saml2;

import static com.github.choonchernlim.betterPreconditions.preconditions.PreconditionFactory.expect;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.protocol.Protocol;
import org.apache.commons.httpclient.protocol.ProtocolSocketFactory;
import org.apache.velocity.app.VelocityEngine;
import org.opensaml.common.xml.SAMLConstants;
import org.opensaml.saml2.metadata.provider.HTTPMetadataProvider;
import org.opensaml.saml2.metadata.provider.MetadataProvider;
import org.opensaml.saml2.metadata.provider.MetadataProviderException;
import org.opensaml.xml.parse.StaticBasicParserPool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.saml.SAMLAuthenticationProvider;
import org.springframework.security.saml.SAMLBootstrap;
import org.springframework.security.saml.SAMLDiscovery;
import org.springframework.security.saml.SAMLEntryPoint;
import org.springframework.security.saml.SAMLLogoutFilter;
import org.springframework.security.saml.SAMLLogoutProcessingFilter;
import org.springframework.security.saml.SAMLProcessingFilter;
import org.springframework.security.saml.SAMLWebSSOHoKProcessingFilter;
import org.springframework.security.saml.context.SAMLContextProviderLB;
import org.springframework.security.saml.key.JKSKeyManager;
import org.springframework.security.saml.key.KeyManager;
import org.springframework.security.saml.log.SAMLDefaultLogger;
import org.springframework.security.saml.metadata.CachingMetadataManager;
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.processor.HTTPArtifactBinding;
import org.springframework.security.saml.processor.HTTPPAOS11Binding;
import org.springframework.security.saml.processor.HTTPPostBinding;
import org.springframework.security.saml.processor.HTTPRedirectDeflateBinding;
import org.springframework.security.saml.processor.HTTPSOAP11Binding;
import org.springframework.security.saml.processor.SAMLBinding;
import org.springframework.security.saml.processor.SAMLProcessorImpl;
import org.springframework.security.saml.trust.httpclient.TLSProtocolConfigurer;
import org.springframework.security.saml.trust.httpclient.TLSProtocolSocketFactory;
import org.springframework.security.saml.userdetails.SAMLUserDetailsService;
import org.springframework.security.saml.util.VelocityFactory;
import org.springframework.security.saml.websso.ArtifactResolutionProfileImpl;
import org.springframework.security.saml.websso.SingleLogoutProfile;
import org.springframework.security.saml.websso.SingleLogoutProfileImpl;
import org.springframework.security.saml.websso.WebSSOProfile;
import org.springframework.security.saml.websso.WebSSOProfileConsumer;
import org.springframework.security.saml.websso.WebSSOProfileConsumerHoKImpl;
import org.springframework.security.saml.websso.WebSSOProfileConsumerImpl;
import org.springframework.security.saml.websso.WebSSOProfileECPImpl;
import org.springframework.security.saml.websso.WebSSOProfileImpl;
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.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.annotation.PostConstruct;
import java.util.Timer;

/**
 * Spring Security configuration to authenticate against ADFS using SAML protocol.
 * This class should be extended by Sp's Java-based Spring configuration for web security.
 */
public abstract class SAMLWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

    /**
     * Provides an opportunity for child class to access Spring environment, if needed.
     */
    @Autowired
    protected Environment env;

    @Autowired
    private SAMLAuthenticationProvider samlAuthenticationProvider;

    // Initialization of OpenSAML library, must be static to prevent "ObjectPostProcessor is a required bean" exception
    // By default, Spring Security SAML uses SHA-1. So, use `DefaultSAMLBootstrap` to use SHA-256.
    @Bean
    public static SAMLBootstrap samlBootstrap() {
        return new DefaultSAMLBootstrap();
    }

    /**
     * Sp's SAMLConfigBean to further customize this security configuration.
     *
     * @return SAML config bean
     */
    @Bean
    protected abstract SAMLConfigBean samlConfigBean();

    /**
     * Fluent API that pre-configures HttpSecurity with SAML specific configuration.
     *
     * @param http HttpSecurity instance
     * @return Same HttpSecurity instance
     * @throws Exception
     */
    // CSRF must be disabled when processing /saml/** to prevent "Expected CSRF token not found" exception.
    // See: http://stackoverflow.com/questions/26508835/spring-saml-extension-and-spring-security-csrf-protection-conflict/26560447
    protected final HttpSecurity samlizedConfig(final HttpSecurity http) throws Exception {
        return http
                .httpBasic().authenticationEntryPoint(samlEntryPoint())
                .and()
                .csrf().ignoringAntMatchers("/saml/**")
                .and()
                .authorizeRequests().antMatchers("/saml/**").permitAll()
                .and()
                .addFilterBefore(metadataGeneratorFilter(), ChannelProcessingFilter.class)
                .addFilterAfter(filterChainProxy(), BasicAuthenticationFilter.class);
    }

    /**
     * Mocks security by hardcoding a given user so that it will always appear that user is accessing the protected
     * resources. This is useful to allow developer to bypass any web authentication against ADFS during rapid
     * app development.
     *
     * @param http HttpSecurity instance
     * @param user User instance
     * @return HttpSecurity that will never authenticate against ADFS
     */
    protected final HttpSecurity mockSecurity(final HttpSecurity http, final User user) {
        expect(user, "user").not().toBeNull().check();

        if (samlConfigBean().getSamlUserDetailsService() == null) {
            throw new SpringSecurityAdfsSaml2Exception(
                    "`samlConfigBean.samlUserDetailsService` cannot be null. " +
                    "When mocking security, the given user details object will be set as principal. " +
                    "Because setting `samlConfigBean.samlUserDetailsService` will set the user details object as principal, " +
                    "this property must be configured to ensure the mock security mimics the actual security configuration."
            );
        }

        return http.addFilterBefore(new MockFilterSecurityInterceptor(user), FilterSecurityInterceptor.class);
    }

    /**
     * Fluent API that pre-configures WebSecurity with SAML specific configuration.
     *
     * @param web WebSecurity instance
     * @return Same WebSecurity instance
     * @throws Exception
     */
    protected final WebSecurity samlizedConfig(final WebSecurity web) throws Exception {
        web.ignoring().antMatchers(samlConfigBean().getSuccessLogoutUrl());
        return web;
    }

    // IDP metadata URL
    private String getMetdataUrl() {
        return String.format("https://%s/federationmetadata/2007-06/federationmetadata.xml",
                             samlConfigBean().getIdpServerName());
    }

    // Entry point to initialize authentication
    @Bean
    public SAMLEntryPoint samlEntryPoint() {
        WebSSOProfileOptions webSSOProfileOptions = new WebSSOProfileOptions();

        // Disable element scoping when sending requests to IdP to prevent
        // "Response has invalid status code urn:oasis:names:tc:SAML:2.0:status:Responder, status message is null"
        // exception
        webSSOProfileOptions.setIncludeScoping(false);

        // Always use HTTP-Redirect instead of HTTP-Post, although both works with ADFS
        webSSOProfileOptions.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);

        // Force IdP to re-authenticate user if issued token is too old to prevent
        // "Authentication statement is too old to be used with value" exception
        // See: http://stackoverflow.com/questions/30528636/saml-login-errors
        webSSOProfileOptions.setForceAuthN(true);

        // Determine what authentication method to use (WIA, user/password, etc).
        // If not set, it will use authentication method order defined by IdP
        if (!samlConfigBean().getAuthnContexts().isEmpty()) {
            webSSOProfileOptions.setAuthnContexts(samlConfigBean().getAuthnContexts());
        }

        SAMLEntryPoint samlEntryPoint = new SAMLEntryPoint();
        samlEntryPoint.setDefaultProfileOptions(webSSOProfileOptions);

        return samlEntryPoint;
    }

    // Filter automatically generates default SP metadata
    @Bean
    public MetadataGeneratorFilter metadataGeneratorFilter() {
        // generates base URL that matches `SAMLContextProviderLB` configuration
        // to ensure SAML endpoints work for server doing SSL termination
        StringBuilder sb = new StringBuilder();
        sb.append("https://").append(samlConfigBean().getSpServerName());
        if (samlConfigBean().getSpHttpsPort() != 443) {
            sb.append(":").append(samlConfigBean().getSpHttpsPort());
        }
        sb.append(samlConfigBean().getSpContextPath());
        String entityBaseUrl = sb.toString();

        MetadataGenerator metadataGenerator = new MetadataGenerator();
        metadataGenerator.setKeyManager(keyManager());
        metadataGenerator.setEntityBaseURL(entityBaseUrl);
        return new MetadataGeneratorFilter(metadataGenerator);
    }

    // HTTP client
    @Bean
    public HttpClient httpClient() {
        return new HttpClient(new MultiThreadedHttpConnectionManager());
    }

    // Filters for processing of SAML messages
    @Bean
    public FilterChainProxy filterChainProxy() throws Exception {
        //@formatter:off
        return new FilterChainProxy(ImmutableList.of(
                new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/login/**"), samlEntryPoint()),
                new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/logout/**"), samlLogoutFilter()),
                new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/metadata/**"), metadataDisplayFilter()),
                new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSO/**"), samlProcessingFilter()),
                new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSOHoK/**"), samlWebSSOHoKProcessingFilter()),
                new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SingleLogout/**"), samlLogoutProcessingFilter()),
                new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/discovery/**"), samlIDPDiscovery())
        ));
        //@formatter:on
    }

    // Handler deciding where to redirect user after successful login
    @Bean
    public SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler() {
        SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler = new SavedRequestAwareAuthenticationSuccessHandler();
        successRedirectHandler.setDefaultTargetUrl(samlConfigBean().getSuccessLoginDefaultUrl());
        return successRedirectHandler;
    }

    // Handler deciding where to redirect user after failed login
    @Bean
    public SimpleUrlAuthenticationFailureHandler failureRedirectHandler() {
        SimpleUrlAuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();

        // The precondition on `setDefaultFailureUrl(..)` will cause an exception if the value is null.
        // So, only set this value if it is not null
        if (!samlConfigBean().getFailedLoginDefaultUrl().isEmpty()) {
            failureHandler.setDefaultFailureUrl(samlConfigBean().getFailedLoginDefaultUrl());
        }

        return failureHandler;
    }

    // Handler for successful logout
    @Bean
    public SimpleUrlLogoutSuccessHandler successLogoutHandler() {
        SimpleUrlLogoutSuccessHandler successLogoutHandler = new SimpleUrlLogoutSuccessHandler();
        successLogoutHandler.setDefaultTargetUrl(samlConfigBean().getSuccessLogoutUrl());
        return successLogoutHandler;
    }

    // Authentication manager
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    // Register authentication manager for SAML provider
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(samlAuthenticationProvider);
    }

    // Logger for SAML messages and events
    @Bean
    public SAMLDefaultLogger samlLogger() {
        return new SAMLDefaultLogger();
    }

    // Central storage of cryptographic keys
    @Bean
    public KeyManager keyManager() {
        return new JKSKeyManager(samlConfigBean().getKeystoreResource(),
                                 samlConfigBean().getKeystorePassword(),
                                 ImmutableMap.of(samlConfigBean().getKeystoreAlias(),
                                                 samlConfigBean().getKeystorePrivateKeyPassword()),
                                 samlConfigBean().getKeystoreAlias());
    }

    // IDP Discovery service
    @Bean
    public SAMLDiscovery samlIDPDiscovery() {
        return new SAMLDiscovery();
    }

    // The filter is waiting for connections on URL suffixed with filterSuffix and presents SP metadata there
    @Bean
    public MetadataDisplayFilter metadataDisplayFilter() {
        return new MetadataDisplayFilter();
    }

    // Configure HTTP Client to accept certificates from the keystore instead of JDK keystore  for HTTPS verification
    @Bean
    public TLSProtocolConfigurer tlsProtocolConfigurer() {
        return new TLSProtocolConfigurer();
    }

    // Configure TLSProtocolConfigurer
    @Bean
    public ProtocolSocketFactory protocolSocketFactory() {
        return new TLSProtocolSocketFactory(keyManager(), null, "default");
    }

    // Configure TLSProtocolConfigurer
    @Bean
    public Protocol protocol() {
        return new Protocol("https", protocolSocketFactory(), 443);
    }

    // Configure TLSProtocolConfigurer
    @PostConstruct
    public MethodInvokingFactoryBean socketFactoryInitialization() {
        MethodInvokingFactoryBean methodInvokingFactoryBean = new MethodInvokingFactoryBean();
        methodInvokingFactoryBean.setTargetClass(Protocol.class);
        methodInvokingFactoryBean.setTargetMethod("registerProtocol");
        methodInvokingFactoryBean.setArguments(new Object[]{"https", protocol()});
        return methodInvokingFactoryBean;
    }

    // IDP Metadata configuration - paths to metadata of IDPs in circle of trust is here
    @Bean
    public CachingMetadataManager metadata() throws MetadataProviderException {
        HTTPMetadataProvider httpMetadataProvider = new HTTPMetadataProvider(new Timer(true),
                                                                             httpClient(),
                                                                             getMetdataUrl());
        httpMetadataProvider.setParserPool(parserPool());

        ExtendedMetadataDelegate extendedMetadataDelegate = new ExtendedMetadataDelegate(httpMetadataProvider);
        // Disable metadata trust check to prevent "Signature trust establishment failed for metadata entry" exception
        extendedMetadataDelegate.setMetadataTrustCheck(false);

        return new CachingMetadataManager(ImmutableList.of(extendedMetadataDelegate));
    }

    // SAML Authentication Provider responsible for validating of received SAML messages
    @Bean
    public SAMLAuthenticationProvider samlAuthenticationProvider() {
        SAMLAuthenticationProvider samlAuthenticationProvider = new SAMLAuthenticationProvider();
        SAMLUserDetailsService samlUserDetailsService = samlConfigBean().getSamlUserDetailsService();

        if (samlUserDetailsService != null) {
            samlAuthenticationProvider.setUserDetails(samlUserDetailsService);

            // By default, `principal` is always going to be `NameID` even though the `Authentication` object
            // contain `userDetails` object. So, if `userDetails` is provided, then don't force `principal` as
            // string so that `principal` represents `userDetails` object.
            // See: http://stackoverflow.com/questions/33786861/how-to-override-the-nameid-value-in-samlauthenticationprovider
            samlAuthenticationProvider.setForcePrincipalAsString(false);
        }

        return samlAuthenticationProvider;
    }

    // In order to get SAML to work for Sp servers doing SSL termination, `SAMLContextProviderLB` has
    // to be used instead of `SAMLContextProviderImpl` to prevent the following exception:-
    //
    // "SAML message intended destination endpoint 'https://server/app/saml/SSO' did not match the
    // recipient endpoint 'http://server/app/saml/SSO'"
    //
    // This configuration will work for Sp servers (not) doing SSL termination.
    @Bean
    public SAMLContextProviderLB contextProvider() {
        SAMLContextProviderLB contextProviderLB = new SAMLContextProviderLB();
        contextProviderLB.setScheme("https");
        contextProviderLB.setServerName(samlConfigBean().getSpServerName());
        contextProviderLB.setServerPort(samlConfigBean().getSpHttpsPort());
        contextProviderLB.setIncludeServerPortInRequestURL(samlConfigBean().getSpHttpsPort() != 443);
        contextProviderLB.setContextPath(samlConfigBean().getSpContextPath());
        return contextProviderLB;
    }

    // Processing filter for WebSSO profile messages
    @Bean
    public SAMLProcessingFilter samlProcessingFilter() throws Exception {
        SAMLProcessingFilter samlWebSSOProcessingFilter = new SAMLProcessingFilter();
        samlWebSSOProcessingFilter.setAuthenticationManager(authenticationManager());
        samlWebSSOProcessingFilter.setAuthenticationSuccessHandler(successRedirectHandler());
        samlWebSSOProcessingFilter.setAuthenticationFailureHandler(failureRedirectHandler());
        return samlWebSSOProcessingFilter;
    }

    // Processing filter for WebSSO Holder-of-Key profile
    @Bean
    public SAMLWebSSOHoKProcessingFilter samlWebSSOHoKProcessingFilter() throws Exception {
        SAMLWebSSOHoKProcessingFilter samlWebSSOHoKProcessingFilter = new SAMLWebSSOHoKProcessingFilter();
        samlWebSSOHoKProcessingFilter.setAuthenticationSuccessHandler(successRedirectHandler());
        samlWebSSOHoKProcessingFilter.setAuthenticationManager(authenticationManager());
        samlWebSSOHoKProcessingFilter.setAuthenticationFailureHandler(failureRedirectHandler());
        return samlWebSSOHoKProcessingFilter;
    }

    // Logout handler terminating local session
    @Bean
    public SecurityContextLogoutHandler logoutHandler() {
        SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler();
        logoutHandler.setInvalidateHttpSession(true);
        logoutHandler.setClearAuthentication(true);
        return logoutHandler;
    }

    // Override default logout processing filter with the one processing SAML messages
    @Bean
    public SAMLLogoutFilter samlLogoutFilter() {
        return new SAMLLogoutFilter(successLogoutHandler(),
                                    new LogoutHandler[]{logoutHandler()},
                                    new LogoutHandler[]{logoutHandler()});
    }

    // Filter processing incoming logout messages
    // First argument determines URL user will be redirected to after successful global logout
    @Bean
    public SAMLLogoutProcessingFilter samlLogoutProcessingFilter() {
        return new SAMLLogoutProcessingFilter(successLogoutHandler(), logoutHandler());
    }

    // Class loading incoming SAML messages from httpRequest stream
    @Bean
    public SAMLProcessorImpl processor() {
        return new SAMLProcessorImpl(ImmutableList.of(redirectDeflateBinding(),
                                                                   postBinding(),
                                                                   artifactBinding(),
                                                                   soapBinding(),
                                                                   paosBinding()));
    }

    // SAML 2.0 WebSSO Assertion Consumer
    @Bean
    public WebSSOProfileConsumer webSSOprofileConsumer() {
        return new WebSSOProfileConsumerImpl();
    }

    // SAML 2.0 Holder-of-Key WebSSO Assertion Consumer
    @Bean
    public WebSSOProfileConsumerHoKImpl hokWebSSOprofileConsumer() {
        return new WebSSOProfileConsumerHoKImpl();
    }

    // SAML 2.0 Web SSO profile
    @Bean
    public WebSSOProfile webSSOprofile() {
        return new WebSSOProfileImpl();
    }

    // SAML 2.0 Holder-of-Key Web SSO profile
    @Bean
    public WebSSOProfileConsumerHoKImpl hokWebSSOProfile() {
        return new WebSSOProfileConsumerHoKImpl();
    }

    // SAML 2.0 ECP profile
    @Bean
    public WebSSOProfileECPImpl ecpprofile() {
        return new WebSSOProfileECPImpl();
    }

    // SAML 2.0 Logout profile
    @Bean
    public SingleLogoutProfile logoutprofile() {
        return new SingleLogoutProfileImpl();
    }

    // Bindings, encoders and decoders used for creating and parsing messages
    @Bean
    public HTTPPostBinding postBinding() {
        return new HTTPPostBinding(parserPool(), velocityEngine());
    }

    @Bean
    public HTTPRedirectDeflateBinding redirectDeflateBinding() {
        return new HTTPRedirectDeflateBinding(parserPool());
    }

    @Bean
    public HTTPArtifactBinding artifactBinding() {
        ArtifactResolutionProfileImpl artifactResolutionProfile = new ArtifactResolutionProfileImpl(httpClient());
        artifactResolutionProfile.setProcessor(new SAMLProcessorImpl(soapBinding()));
        return new HTTPArtifactBinding(parserPool(), velocityEngine(), artifactResolutionProfile);
    }

    @Bean
    public HTTPSOAP11Binding soapBinding() {
        return new HTTPSOAP11Binding(parserPool());
    }

    @Bean
    public HTTPPAOS11Binding paosBinding() {
        return new HTTPPAOS11Binding(parserPool());
    }

    // Initialization of the velocity engine
    @Bean
    public VelocityEngine velocityEngine() {
        return VelocityFactory.getEngine();
    }

    // XML parser pool needed for OpenSAML parsing
    @Bean(initMethod = "initialize")
    public StaticBasicParserPool parserPool() {
        return new StaticBasicParserPool();
    }

    @Bean
    public ParserPoolHolder parserPoolHolder() {
        return new ParserPoolHolder();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy