javaspringconfigurationwaffle

How to configure waffle in spring using java configuration


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.


Solution

  • 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.