11

I have been struggling to get waffle to work with spring 4.2.5 using spring java configuration. And I thought I might as well help others in the same situation.

We use a custom preWaffle and postWaffle filter to authenticate that the user exists in our database after it has been validated via waffles NTLM protocol.

We also have methods for authorization of a user actions using the EnableGlobalMethodSecurity annotation.

To get this working in spring java configuration was trouble some to say the least. You can find our solution in the answer below. I hope it will help.

Pablo Jomer
  • 9,870
  • 11
  • 54
  • 102

1 Answers1

13

SpringConfiguration.java

// ... imports 
@Configuration
@EnableWebMvc
@EnableScheduling
@PropertySources({
    @PropertySource("classpath:app.properties")
    // ... Properties sources
})
@EnableTransactionManagement
@ComponentScan(basePackages = "com.our.package")
public class SpringConfiguration extends WebMvcConfigurerAdapter {

 // Our Spring configuration ...

}

SecurityConfiguration.java

// ... imports 
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, proxyTargetClass = true)
@Order(1)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter{

  // Authentication manager configuration
  @Autowired
  private WindowsAuthenticationProviderWrapper authProvider;

  @Autowired
  private AuthenticationManagerBuilder auth;

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

  @Bean
  public AuthenticationManager authenticationManager() throws Exception {
    return auth.getObject();
  }

  // Waffle configuration
  @Bean
  public Filter customPreAuthSecurityFilter() {
    return new CustomPreAuthSecurityFilter();
  }

  @Bean
  public Filter customNegotiateSecurityFilter() {
    return new CustomNegotiateSecurityFilter();
  }

  @Bean
  public WindowsAuthProviderImpl waffleAuthProvider(){
    return new WindowsAuthProviderImpl();
  }

  @Bean(name="negotiateSecurityFilterProvider")
  @Autowired
  public NegotiateSecurityFilterProvider negotiateSecurityFilterProvider(){
    NegotiateSecurityFilterProvider bean = new NegotiateSecurityFilterProvider(waffleAuthProvider());
    List<String> protocols = new ArrayList<>();
    protocols.add("Negotiate");
    bean.setProtocols(protocols);
    return bean;
  }

  @Bean
  public BasicSecurityFilterProvider basicSecurityFilterProvider(){
    return new BasicSecurityFilterProvider(waffleAuthProvider());
  }

  @Bean(name="waffleSecurityFilterProviderCollection")
  @Autowired
  public waffle.servlet.spi.SecurityFilterProviderCollection negotiateSecurityFilterProviderCollection() {
    final List<SecurityFilterProvider> lsp = new ArrayList<>();
    lsp.add(negotiateSecurityFilterProvider());
    lsp.add(basicSecurityFilterProvider());
    return new waffle.servlet.spi.SecurityFilterProviderCollection(lsp.toArray(new SecurityFilterProvider[]{}));
  }

  @Bean(name="negotiateSecurityFilterEntryPoint")
  @Autowired
  public waffle.spring.NegotiateSecurityFilterEntryPoint negotiateSecurityFilterEntryPoint() {
    final waffle.spring.NegotiateSecurityFilterEntryPoint ep = new waffle.spring.NegotiateSecurityFilterEntryPoint();
    ep.setProvider(negotiateSecurityFilterProviderCollection());
    return ep;
  }

  @Bean(name="negotiateSecurityFilter")
  @Autowired
  public waffle.spring.NegotiateSecurityFilter waffleNegotiateSecurityFilter(){
    waffle.spring.NegotiateSecurityFilter bean = new waffle.spring.NegotiateSecurityFilter();
    bean.setRoleFormat("both");
    bean.setPrincipalFormat("fqn");
    bean.setAllowGuestLogin(false);
    bean.setProvider(negotiateSecurityFilterProviderCollection());
    return bean;
  }

  // Static Mappings
  @Override
  public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/assets/**");
  }

  // Security filter chain
  // The custom filters can be removed if you only use waffle 
  // but this is how we added them
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    // A user needs to have the role user and has to be authenticated
    http.exceptionHandling()
      .authenticationEntryPoint(negotiateSecurityFilterEntryPoint()).and()
      .addFilterBefore(customPreAuthSecurityFilter(), BasicAuthenticationFilter.class)
      .addFilterAfter(waffleNegotiateSecurityFilter(), BasicAuthenticationFilter.class)
      .addFilterAfter(customNegotiateSecurityFilter(), BasicAuthenticationFilter.class)
      .authorizeRequests().anyRequest().fullyAuthenticated();
     }
  }

To be able to autowire the waffle authProvider I created the following wrapperclass.

WindowsAuthenticationProviderWrapper.java

// ... imports 
// This class purpose is only to make the Windows authentication provider autowireable in spring.
@Component
public class WindowsAuthenticationProviderWrapper extends WindowsAuthenticationProvider{}

As requested (Some code has been stripped due to security risks).

CustomPreAuthFilter.java

import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * This filter removes the excess negoatiate header sent by IE. If the client
 * has already authenticated, strip the Authorization header from the request.
 */
public class CustomPreAuthSecurityFilter extends GenericFilterBean {
  @Override
  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    SecurityContext sec = SecurityContextHolder.getContext();
    HttpServletRequest req = (HttpServletRequest) servletRequest;

    if(sec != null && sec.getAuthentication() != null) {
      req = new CustomServletRequestWrapper(req);
    }

    try {
      filterChain.doFilter(req, servletResponse);
    } catch (RuntimeException e) {
      sendUnauthorized((HttpServletResponse) servletResponse);
    }
  }

  private void sendUnauthorized(HttpServletResponse response) throws IOException {
    logger.warn("error logging in user");
    SecurityContextHolder.clearContext();
    response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
  }
}

CustomNegotiateSecurityFilter.java

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;
import waffle.servlet.WindowsPrincipal;
import waffle.spring.WindowsAuthenticationToken;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;

/**
 * Handle post NTLM authentication against system database
 */
public class CustomNegotiateSecurityFilter extends GenericFilterBean {

  @Autowired
  private UserDAO userDAO;

  @Autowired
  Environment env;

  private static final Logger LOGGER = LoggerFactory.getLogger(CustomNegotiateSecurityFilter.class);

  public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
    final HttpServletRequest request = (HttpServletRequest) req;
    final HttpServletResponse response = (HttpServletResponse) res;
    SecurityContext sec = SecurityContextHolder.getContext();
    Authentication authentication = sec.getAuthentication();

    // Continue filter chain if we are anonymously authenticated or if DB authentication has already happened.
    if (authentication != null && authentication.getClass() == WindowsAuthenticationToken.class) {

      // The user is Authenticated with NTLM but needs to be checked against the DB.
      User user;

      try {
        // fetch user from DB ...
      } catch (Exception e) {
        // The could not be found in the DB.
        sendUnauthorized(response);
        return;
      }

      // The user was found in the DB.
      WindowsPrincipal principal = (WindowsPrincipal)authentication.getPrincipal();
      final CustomAuthenticationToken token = new CustomAuthenticationToken(principal); // This class extends WindowsAuthenticationToken

      // add roles to token ...

      sec.setAuthentication(token);
    }

    chain.doFilter(request, response);
  }

  private void sendUnauthorized(HttpServletResponse response) throws IOException {
    logger.warn("Could not log in user");
    SecurityContextHolder.clearContext();
    response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
  }

  private void addRoleToAuthentication(WindowsAuthenticationToken authentication, String role) {
      for(GrantedAuthority authority : authentication.getAuthorities()) {
        if(authority.getAuthority().equals(role)) {
          return;
        }
      }
      authentication.getAuthorities().add(new SimpleGrantedAuthority(role));
  }
}

EDIT

For those who asked about here is one implementation. CustomServletRequestWrapper:

class CustomServletRequestWrapper extends HttpServletRequestWrapper {


    public CustomServletRequestWrapper(HttpServletRequest request) {
        super(request);
    }
    public String getHeader(String name) {
        if(name.equals("Authorization"))
            return null;
        String header = super.getHeader(name);
        return (header != null) ? header : super.getParameter(name); // Note: you can't use getParameterValues() here.
    }

    public Enumeration getHeaderNames() {
        List<String> names = Collections.list(super.getHeaderNames());

        names.addAll(Collections.list(super.getParameterNames()));
        names.remove("Authorization");
        return Collections.enumeration(names);
    }

}

If you need more information don't hessitate to ask.

Pablo Jomer
  • 9,870
  • 11
  • 54
  • 102
  • Im trying to implement your code but im getting: Factory method 'springSecurityFilterChain' threw exception; nested exception is org.springframework.security.config.annotation.AlreadyBuiltException: This object has already been built – naoru Aug 02 '16 at 22:53
  • I don't have access to the code base any more so I can't check. I do remember having some similar issues. I guess this is because you have already created the object. Check to se why it is created twice and If you find any flaws please report them here. – Pablo Jomer Aug 06 '16 at 22:20
  • @PabloJomer - Thank you for the answer. However, I get "waffle.spring.NegotiateSecurityFilter : successfully logged in user: domain\username" and "waffle.spring.NegotiateSecurityFilter : error logging in user: com.sun.jna.platform.win32.Win32Exception: The token supplied to the function is invalid" the first time I try. A browser refresh after works fine. – JHS Sep 07 '16 at 12:40
  • @JHS is everything initialized at startup or does the component initialize on the first request? That could lead to this type of problem. – Pablo Jomer Sep 07 '16 at 14:20
  • @PabloJomer - This happens for every user coming in for the first time. Don't know why. Did some search here and there and found no satisfactory answer. – JHS Sep 07 '16 at 15:56
  • Is this happening in all browsers? I know we had a workaround for an IE issue all though I can't access the code any more. – Pablo Jomer Sep 07 '16 at 16:36
  • @JHS were you able to solve your issue? im having the exact sometimes first hit gives me that error and a refresh resolves it – naoru Sep 22 '16 at 16:06
  • Can you provide example of implementation of the CustomPreAuthSecurityFilter and CustomNegotiateSecurityFilter? – ronielhcuervo Oct 19 '16 at 22:34
  • I currently don't have access to the code I will however ask my old colleagues if they could send me these snippets. Return to you if I get them today. – Pablo Jomer Oct 20 '16 at 07:01
  • @ronielhcuervo I added the files you requested. Some code has been filtered and comments have been left in place. – Pablo Jomer Oct 20 '16 at 19:21
  • 1
    @pablo-jomer Thanks for the quick response, I'm curious for the code of CustomServletRequestWrapper, but I see that you no longer has access to the source code... Thankss – ronielhcuervo Oct 20 '16 at 20:54
  • @PabloJomer.. I understand its been almost a year you have fixed this, but I was facing issue in the waffle sso implementation and came across this post, so thought to post in case you can recollect. I have implemented all the filters and wrappers as suggested, however I am not getting Security Package or user details in the Servlet Request. Not sure if there is any browser setting or any other setting that needs to be done? Appreciate your help – vnkotak Sep 25 '17 at 13:56
  • I remember having some problems with IE browsers. Also bare in mind that the API may have changed. – Pablo Jomer Oct 01 '17 at 05:50