6
votes

Using pure Spring Java Config I'm having troubles getting Spring and CAS to perform Single Sign Out. I have Single Sign On working with the configuration below. I use a simple JSP page to do a form POST to the url https://nginx.shane.com/app/logout and I include the CSRF value in the POST'd data. It all appears to work with no errors but when I go to a secured page it just lets me back in without requiring to login. Any ideas?

@Configuration
@EnableWebSecurity
public class SecurityWebAppConfig extends WebSecurityConfigurerAdapter {

@Bean
protected ServiceProperties serviceProperties() {
    ServiceProperties serviceProperties = new ServiceProperties();
    serviceProperties.setService("https://nginx.shane.com/app/j_spring_cas_security_check");
    serviceProperties.setSendRenew(false);
    return serviceProperties;
}

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

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

@Bean
public Cas20ServiceTicketValidator cas20ServiceTicketValidator() {
    return new Cas20ServiceTicketValidator("https://nginx.shane.com/cas");
}

@Bean
public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
    CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
    casAuthenticationFilter.setAuthenticationManager(authenticationManager());
    return casAuthenticationFilter;
}

@Bean
public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {
    CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint();
    casAuthenticationEntryPoint.setLoginUrl("https://nginx.shane.com/cas/login");
    casAuthenticationEntryPoint.setServiceProperties(serviceProperties());

    return casAuthenticationEntryPoint;
}

@Bean
public SingleSignOutFilter singleSignOutFilter() {
    // This filter handles a Single Logout Request from the CAS Server
    return new SingleSignOutFilter();
}

@Bean
public LogoutFilter requestLogoutFilter() {
    // This filter redirects to the CAS Server to signal Single Logout should be performed
    SecurityContextLogoutHandler handler = new SecurityContextLogoutHandler();
    handler.setClearAuthentication(true);
    handler.setInvalidateHttpSession(true);

    LogoutFilter logoutFilter = new LogoutFilter("https://nginx.shane.com/", handler);
    return logoutFilter;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.addFilter(casAuthenticationFilter());
    http.addFilterBefore(requestLogoutFilter(), LogoutFilter.class);
    http.addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class);

    http.exceptionHandling()
        .authenticationEntryPoint(casAuthenticationEntryPoint());

    http.authorizeRequests()
        .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
        .antMatchers("/dba/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_DBA')");

    http.logout()
        .addLogoutHandler(handler)
        .deleteCookies("remove")
        .invalidateHttpSession(true)
        .logoutUrl("/logout")
        .logoutSuccessUrl("/");
}

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

}

I also added a WebListener to handle the session destroyed event:

@WebListener
public class SecurityWebListener implements HttpSessionListener {

private SingleSignOutHttpSessionListener listener = new SingleSignOutHttpSessionListener();

@Override
public void sessionCreated(HttpSessionEvent se) {
    listener.sessionCreated(se);
}

@Override
public void sessionDestroyed(HttpSessionEvent se) {
    listener.sessionDestroyed(se);
}
}

Here's the log output

[org.springframework.security.web.FilterChainProxy] [/logout at position 6 of 14 in additional filter chain; firing Filter: 'LogoutFilter'] []
[org.springframework.security.web.util.matcher.AntPathRequestMatcher] [Checking match of request : '/logout'; against '/logout'] []
[org.springframework.security.web.authentication.logout.LogoutFilter] [Logging out user 'org.springframework.security.cas.authentication.CasAuthenticationToken@836ad34b: Principal: org.springframework.security.core.userdetails.User@586034f: Username: admin; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_ADMIN; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@fffdaa08: RemoteIpAddress: 127.0.0.1; SessionId: FA880C15EF09C033E1CA0C8E4785905F; Granted Authorities: ROLE_ADMIN Assertion: org.jasig.cas.client.validation.AssertionImpl@fcd38ec Credentials (Service/Proxy Ticket): ST-23-1UandqRxBcG6HCTx0Pdd-cas01.example.org' and transferring to logout destination] []
[org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler] [Invalidating session: FA880C15EF09C033E1CA0C8E4785905F] []
[org.jasig.cas.client.session.HashMapBackedSessionMappingStorage] [Attempting to remove Session=[FA880C15EF09C033E1CA0C8E4785905F]] []
[org.jasig.cas.client.session.HashMapBackedSessionMappingStorage] [Found mapping for session.  Session Removed.] []
[org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler] [Using default Url: /] []
[org.springframework.security.web.DefaultRedirectStrategy] [Redirecting to '/app/'] []
1

1 Answers

3
votes

(Un)lucky I had similar problem;) It occures when CAS tries to call your application to log out. On the one hand CAS tries to pass sessionId to perform logout, on the other hand SpringSecurity expects to obtain CSRF token, which was not send by CAS as it sends GET request only. CsrfFilter doesn't find csrf token and interrupts the filter chain. User is not aware of that since CAS calls logout request implicitly. Request goes directly from CAS server to the application server, not by redirecting user in the Web browser.

In order to make it work you need to configure HttpSecurity to exclude/not to include LogoutFilter filterProcessesUrl (which is j_spring_security_logout in your case as you use the default one).

Assuming that you want to check CSRF when trying to create new admin, for insatnce, you need to configure it as follows:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.addFilter(casAuthenticationFilter());
    http.addFilterBefore(requestLogoutFilter(), LogoutFilter.class);
    http.addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class);

    http.exceptionHandling()
        .authenticationEntryPoint(casAuthenticationEntryPoint());

    http.authorizeRequests()
        .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
        .antMatchers("/dba/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_DBA')");

    http.csrf()
        .requireCsrfProtectionMatcher(new AntPathRequestMatcher("/admin/create"));

    http.logout()
        .addLogoutHandler(handler)
        .deleteCookies("remove")
        .invalidateHttpSession(true)
        .logoutUrl("/logout")
        .logoutSuccessUrl("/");
}

Just to indicate, i have added

http.csrf().requireCsrfProtectionMatcher(new AntPathRequestMatcher("/admin/create"));.

Pay attention that you cannot use match all pattern (/admin/**) since you presumably want to call some get requests as well and CSRF filter will expect them to send the token then.

Such problem will not arise with Spring Security previous than 3.2.x, since the Cross Site Request Forgery (CSRF) support was introduced there.

Hope these help:)