spring-projects / spring-security

Spring Security
http://spring.io/projects/spring-security
Apache License 2.0
8.79k stars 5.9k forks source link

Redirection not happening to original request after proxy ticket validation #4158

Open srinivasthalapalli opened 7 years ago

srinivasthalapalli commented 7 years ago

I am using CAS proxy tickets in my applications, the proxy ticket receiving application (from which I am expecting resource) validates the proxy ticket and never goes back to original target url.

After successful validation of proxy ticket in CASAuthenticationProvider, AbstractAuthenticationProcessingFilter.successfulAuthentication is not called due to final implementation of successfulAuthentication() in CASAuthenticationFilter

from CASAuthenticationFilter.successfulAuthentication()

boolean continueFilterChain = proxyTicketRequest( serviceTicketRequest(request, response), request); if (!continueFilterChain) { super.successfulAuthentication(request, response, chain, authResult); return; }

In above code, after proxy ticket validation, continueFilterChain is always true and super.successfulAuthentication(request, response, chain, authResult); never gets executed and never invokes , SavedRequestAwareAuthenticationSuccessHandler to goto target url.

I am thinking it is a bug, please correct me if I am wrong.

Following is my spring configuration.

`@Bean public SimpleUrlAuthenticationFailureHandler simpleUrlAuthenticationFailureHandler() { SimpleUrlAuthenticationFailureHandler simpleUrlAuthenticationFailureHandler = new SimpleUrlAuthenticationFailureHandler(); return simpleUrlAuthenticationFailureHandler; }

@Bean
public SavedRequestAwareAuthenticationSuccessHandler savedRequestAwareAuthenticationSuccessHandler() {
    SavedRequestAwareAuthenticationSuccessHandler savedRequestAwareAuthenticationSuccessHandler = new SavedRequestAwareAuthenticationSuccessHandler();
    savedRequestAwareAuthenticationSuccessHandler.setDefaultTargetUrl("/");
    return savedRequestAwareAuthenticationSuccessHandler;
}

@Bean
public ServiceProperties serviceProperties() {
    ServiceProperties serviceProperties = new ServiceProperties();
    serviceProperties.setService(serviceUrl);
    serviceProperties.setAuthenticateAllArtifacts(true);
    return serviceProperties;
}

@Bean
public Cas20ProxyTicketValidator cas20ProxyTicketValidator() {
    Cas20ProxyTicketValidator validator = new Cas20ProxyTicketValidator(validationUrl);
    validator.setProxyGrantingTicketStorage(proxyGrantingTicketStorage());
    validator.setAcceptAnyProxy(true);
    return validator;
}

@Bean
public SecurityContextLogoutHandler securityContextLogoutHandler() {
    SecurityContextLogoutHandler securityContextLogoutHandler = new SecurityContextLogoutHandler();
    return securityContextLogoutHandler;
}

@Bean
@DependsOn("securityContextLogoutHandler")
public LogoutFilter requestSingleLogoutFilter() {
    LogoutFilter requestSingleLogoutFilter = new LogoutFilter(logoutUrl, securityContextLogoutHandler());
    requestSingleLogoutFilter.setFilterProcessesUrl("/j_spring_cas_security_logout");
    return requestSingleLogoutFilter;
}

@Bean
public SingleSignOutFilter singleLogoutFilter() {
    SingleSignOutFilter singleLogoutFilter = new SingleSignOutFilter();
    return singleLogoutFilter;
}

@Bean
public ProxyGrantingTicketStorage proxyGrantingTicketStorage() {
    ProxyGrantingTicketStorage proxyGrantingTicketStorage = new ProxyGrantingTicketStorageImpl();
    return proxyGrantingTicketStorage;
}

@Bean
public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
    CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
    casAuthenticationFilter.setProxyGrantingTicketStorage(proxyGrantingTicketStorage());
    casAuthenticationFilter.setProxyReceptorUrl("/j_spring_cas_security_proxyreceptor");
    casAuthenticationFilter.setAuthenticationManager(authenticationManagerBean());
    casAuthenticationFilter.setServiceProperties(serviceProperties());
    casAuthenticationFilter.setAuthenticationDetailsSource(dynamicServiceResolver());
    casAuthenticationFilter.setAuthenticationSuccessHandler(savedRequestAwareAuthenticationSuccessHandler());
    casAuthenticationFilter.setAuthenticationFailureHandler(simpleUrlAuthenticationFailureHandler());
    return casAuthenticationFilter;
}

@Bean
AuthenticationDetailsSource<HttpServletRequest, ServiceAuthenticationDetails> dynamicServiceResolver() {
    return new AuthenticationDetailsSource<HttpServletRequest, ServiceAuthenticationDetails>() {
        public ServiceAuthenticationDetails buildDetails(HttpServletRequest context) {
            final String url = makeDynamicUrlFromRequest(context);
            return new ServiceAuthenticationDetails() {
                public String getServiceUrl() {
                    return url;
                }
            };
        }
    };
}

@Bean
public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {
    CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint() {
        @Override
        protected String createServiceUrl(final HttpServletRequest request, final HttpServletResponse response) {
            return CommonUtils.constructServiceUrl(request, response, null, casServer,
                    serviceProperties().getArtifactParameter(), false);
        }
    };
    casAuthenticationEntryPoint.setLoginUrl(loginUrl);
    casAuthenticationEntryPoint.setServiceProperties(serviceProperties());
    return casAuthenticationEntryPoint;
}

@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
    CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
    casAuthenticationProvider.setAuthenticationUserDetailsService(authenticationUserDetailsService());
    casAuthenticationProvider.setServiceProperties(serviceProperties());
    casAuthenticationProvider.setTicketValidator(cas20ProxyTicketValidator());
    casAuthenticationProvider.setKey("test");
    return casAuthenticationProvider;
}

@Bean(name = "authenticationManager")
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(casAuthenticationProvider());
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable().addFilter(casAuthenticationFilter())
            .addFilterBefore(requestSingleLogoutFilter(), LogoutFilter.class)
            .addFilterBefore(singleLogoutFilter(), CasAuthenticationFilter.class).authorizeRequests().anyRequest()
            .authenticated().and().formLogin().usernameParameter("j_username").passwordParameter("j_password").and()
            .exceptionHandling().authenticationEntryPoint(casAuthenticationEntryPoint());

    http.headers().contentTypeOptions().xssProtection().cacheControl().frameOptions()
            .addHeaderWriter(new HstsHeaderWriter(60 * 60, false));
}

@Bean
public AuthenticationUserDetailsService<CasAssertionAuthenticationToken> authenticationUserDetailsService() {
    return new CasUserPrincipalService();
}

private String makeDynamicUrlFromRequest(HttpServletRequest request) {
    return casServer + request.getRequestURI();
}`

Version

Spring security 3.2.4

lucastheisen commented 6 years ago

This also happens to me with, and I am NOT using proxy validation at all. I have traced it down to the same decision point:

        boolean continueFilterChain = proxyTicketRequest(
                serviceTicketRequest(request, response), request);
        if (!continueFilterChain) {
            super.successfulAuthentication(request, response, chain, authResult);
            return;

The continueFilterChain is always true, so super.succesfulAuthentication never gets called and the associated SavedRequestAwareAuthenticationSuccessHandler never redirects to the originally requested URL.

In my situation, I left proxy config off (as I dont want my app to support proxy configuration):

package org.mitre.facilitysafety.reportviewer.configuration;

import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.validation.Cas30ProxyTicketValidator;
import org.mitre.facilitysafety.reportviewer.auth.CasUserDetailsService;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.authentication.CasAuthenticationProvider;
import org.springframework.security.cas.web.CasAuthenticationEntryPoint;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;

/**
 * CAS configuration based upon the sample XML configuration from the spring
 * security project.
 * 
 * @see <a href=
 *      "https://github.com/spring-projects/spring-security/blob/4.2.3.RELEASE/samples/xml/cas/cassample/src/main/webapp/WEB-INF/applicationContext-security.xml">XML
 *      configuration</a>
 */
@Configuration
@ComponentScan( basePackages = {
        "org.mitre.facilitysafety.reportviewer.configuration" } )
public class CasConfiguration {
    /** This services URL base (ex: https://app.example.com/foo) **/
    @Value( "${cas.service}" )
    @Qualifier( "casService" )
    private String casService;
    /** The CAS server URL base (ex: https://sso.example.com/cas) **/
    @Value( "${cas.server}" )
    @Qualifier( "casServer" )
    private String casServer;

    @Bean
    public CasAuthenticationProvider casAuthenticationProvider(
            ServiceProperties casServiceProperties ) {
        CasAuthenticationProvider provider = new CasAuthenticationProvider();
        provider.setKey( "casAuthProviderKey" );
        provider.setServiceProperties( casServiceProperties );
        provider.setAuthenticationUserDetailsService( new CasUserDetailsService() );
        provider.setTicketValidator( new Cas30ProxyTicketValidator( casServer ) );
        return provider;
    }

    /**
     * Returns the CAS entry point (ie: entry-point-ref).
     * 
     * @param serviceProperties
     *            The service properties
     * @return The CAS entry point
     * 
     * @see <a href=
     *      "https://github.com/spring-projects/spring-security/blob/4.2.3.RELEASE/samples/xml/cas/cassample/src/main/webapp/WEB-INF/applicationContext-security.xml#L55-L57">casEntryPoint</a>
     * @see <a href="https://stackoverflow.com/a/24691937/516433">How to set
     *      entry-point-ref with java config</a>
     * @see <a href=
     *      "https://spring.io/guides/tutorials/spring-boot-oauth2">Example of
     *      setting entry-point-ref with java config</a>
     */
    @Bean
    public CasAuthenticationEntryPoint casEntryPoint(
            ServiceProperties casServiceProperties ) {
        CasAuthenticationEntryPoint entryPoint = new CasAuthenticationEntryPoint();
        entryPoint.setServiceProperties( casServiceProperties );
        entryPoint.setLoginUrl( casServer + "/login" );
        return entryPoint;
    }

    /**
     * This CAS authentication filter.  Need authenticaitonManager, but it
     * <em>MUST</em> be <code>@Lazy</code> or it will result in a circular
     * reference when autowiring into the SecurityConfiguration.
     * 
     * @return A CAS authentication filter
     * @see <a href=
     *      "https://github.com/spring-projects/spring-security/blob/4.2.3.RELEASE/samples/xml/cas/cassample/src/main/webapp/WEB-INF/applicationContext-security.xml#L58-L73">requestSingleLogoutFilter</a>
     */
    @Bean
    public CasAuthenticationFilter casFilter( ServiceProperties casServiceProperties,
            @Lazy AuthenticationManager authenticationManager ) {
        CasAuthenticationFilter filter = new CasAuthenticationFilter();
        filter.setAuthenticationManager( authenticationManager );
        filter.setServiceProperties( casServiceProperties );
        filter.setAuthenticationDetailsSource( new ServiceAuthenticationDetailsSource( casServiceProperties ) );
        return filter;
    }

    /**
     * This filter redirects to the CAS Server to signal Single Logout should be
     * performed.
     * 
     * @return A logout filter
     * @see <a href=
     *      "https://github.com/spring-projects/spring-security/blob/4.2.3.RELEASE/samples/xml/cas/cassample/src/main/webapp/WEB-INF/applicationContext-security.xml#L43-L49">requestSingleLogoutFilter</a>
     */
    @Bean
    public LogoutFilter casRequestSingleLogoutFilter() {
        LogoutFilter filter = new LogoutFilter( casServer + "/logout",
                new SecurityContextLogoutHandler() );
        filter.setFilterProcessesUrl( "/logout/cas" );
        return filter;
    }

    /**
     * Returns the service properties for CAS.
     * 
     * @return The service properties for CAS
     * @see <a href=
     *      "https://github.com/spring-projects/spring-security/blob/4.2.3.RELEASE/samples/xml/cas/cassample/src/main/webapp/WEB-INF/applicationContext-security.xml#L51-L54">serviceProperties</a>
     */
    @Bean
    public ServiceProperties casServiceProperties() {
        ServiceProperties properties = new ServiceProperties();
        properties.setService( casService + "/login" );
        properties.setAuthenticateAllArtifacts( true );
        return properties;
    }

    @Bean
    public SingleSignOutFilter casSingleLogoutFilter() {
        SingleSignOutFilter filter = new SingleSignOutFilter();
        filter.setCasServerUrlPrefix( casServer + "/logout" );
        filter.setIgnoreInitConfiguration( true );
        return filter;
    }
}
lucastheisen commented 6 years ago

I can confirm, that after removing the successfulAuthentication method (Lines 215-240) allows pass through to the super class which behaves as expected. Looking at the history of this file, that section was added to support proxy validation. That said, i am honestly at a loss trying to figure out what this is supposed to do:

       boolean continueFilterChain = proxyTicketRequest(serviceTicketRequest(request, response),request);
       if(!continueFilterChain) {
           super.successfulAuthentication(request, response, chain, authResult);
           return;
       }

Just reading the code, it seems like it make sense. I would assume that the proxyTicketRequest would return true if this was a request containing a proxy ticket. However, it does not behave that way. The proxyTicketRequest method starts by checking if it is a serviceTicketRequest which makes sense (because if its a service ticket request it is not a proxy ticket request:

        if (serviceTicketRequest) {
            return false;
                }

But the serviceTicketRequest method simply checks if authentication is required:

    private boolean serviceTicketRequest(final HttpServletRequest request,
            final HttpServletResponse response) {
        boolean result = super.requiresAuthentication(request, response);
        if (logger.isDebugEnabled()) {
            logger.debug("serviceTicketRequest = " + result);
        }
        return result;
    }

This appears to ignore the fact that authentication already succeeded...

@rwinch , you seem to be the only person committing to the CAS module... Can you shed any light on this? Right now I am creating my own copy of the CasAuthenticationFilter (cant extend because your method overrides are final) that removes the successfulAuthentication override, but that is certainly less than desirable. I would be happy to submit a pull request if i could understand what this code is trying to accomplish...

paul-vautier commented 6 months ago

Hi, the issue still remains. protected final void successfulAuthentication(successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) does not trigger the onSuccessHandler despite the authentication process being approved. We are using authenticated() in private boolean proxyTicketRequest(boolean serviceTicketRequest, HttpServletRequest request) before actually updating authResult. Even if authResult is a valid authentication, we are not yet considered authenticated