davioooh
davioooh

Reputation: 24706

Single sign-on not working in Spring web app

I configured my web application to authenticate via Active Directory repo and it works fine, but I always need to insert credentials in login form.

The application clients will be Windows machines connected to the company network all in the same domain.

I need to configure my web app to authenticate automatically the user that has been yet authenticated in Windows.

I'm using this config for Spring Security:

<security:authentication-manager alias="authenticationManager">
    <security:authentication-provider
        user-service-ref="userDetailsService">
        <security:password-encoder hash="plaintext" />
    </security:authentication-provider> <!-- for DB authentication -->
    <security:authentication-provider
        ref="adAuthenticationProvider" />
</security:authentication-manager>

<bean id="adAuthenticationProvider"
    class="org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider">
    <constructor-arg name="domain" value="mydomain.it" />
    <constructor-arg name="url" value="ldap://domaincontroller.mydomain.it/" />
</bean>

NOTE I also need a secondary authentication provider to provide DB authentication.

I also set the following option in IE (v 9) that should enable automatic logon:

enter image description here

But it doesn't work... so what's wrong in my configuration?

NOTE #2 I'm using Spring v 3.2.9 and Spring Security v 3.2.3

Upvotes: 4

Views: 2704

Answers (3)

Steve
Steve

Reputation: 9490

The 'right' way is to do Kerberos/SPNEGO. However, the server needs to be a trusted node on your Windows domain. If your server is a Windows machine, then that should be easy enough. However, if it's a 'NIX/Linux machine, then it can be a real PITA.

This involves things such as setting it up with an SPN (Service Principal Name) in Active Directory and the installation of a whole pile of stuff on your server to integrate with A/D and authenticate users against it.

If you (or your friendly Windows infrastructure team member) are happy with doing all that, then go for it! However, if you're not familiar with it, I should warn you that when it doesn't work, diagnosing the problem is hell.

However there is a quick and dirty option, which doesn't involve any trust for your server on the network. In fact the whole thing can be wrapped up in your Spring app. That's the not-quite-so-secure-but-it-works NTLM. It's pretty easy to set up.

First you will want a Servlet filter to intercept requests and perform the handshakes if there is no session:

import java.io.IOException;

import org.apache.commons.codec.binary.Base64;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

/**
 * Simple authentication filter designed to get hold of the username via NTLM SSO. 
 * See Spring documentation on pre-authentication filters to see how this can be used.
 * </p>
 * <p>
 * <a href="http://static.springsource.org/spring-security/site/docs/3.1.x/reference/springsecurity-single.html#preauth">http://static.springsource.org/spring-security/site/docs/3.1.x/reference/springsecurity-single.html#preauth</a>
 * </p>
 */
@Component("ntlmFilter")
public class NtlmFilter implements Filter {

    private static Logger log = LoggerFactory.getLogger(NtlmFilter.class);

    public static final String USERNAME_KEY = "SM_USER";

    public NtlmFilter() {
        log.info("Initialising the NTLM filter.");
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // No initialisation tasks.
    }

    @Override
    public void destroy() {
        // No destruction tasks.
    }

    /**
     * 
     */
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        if (isAuthenticated(request)) {
            log.debug("Session already authenticated. Proceeding down filter chain.");
            setRequestHeaders(request);
            proceed(req, res, chain);
        } else {
            log.debug("Session not yet authenticated. Attempting to login...");
            login(request, response, chain);
        }
    }

    private void proceed(ServletRequest req, ServletResponse res, FilterChain chain) 
            throws IOException, ServletException {
        try {
            chain.doFilter(req, res);
        } catch (IOException e) {
            log.error("IOException processing NtlmAuthFilter Servlet filter.", e);
            throw e;
        } catch (ServletException e) {
            log.error("ServletException processing NtlmAuthFilter Servlet filter.", e);
            throw e;
        }
    }

    /**
     * If the user name has been stored in the session, then the user has been
     * authenticated by the application.
     */
    private boolean isAuthenticated(HttpServletRequest req) {
        if (req.getSession().getAttribute(USERNAME_KEY) != null) {
            return true;
        } else {
            return false;
        }
    }

    public void login(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws ServletException, IOException {

        String username = null;

        String auth = req.getHeader("Authorization");
        if (auth == null) {
            // First phase. Return NTLM challenge headers.
            res.setHeader("WWW-Authenticate", "NTLM");
            res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            res.setContentLength(0);
            res.flushBuffer();
            return;
        } else if (auth.startsWith("NTLM ")) {
            byte[] msg = Base64.decodeBase64(auth.substring(5));
            int off = 0, length, offset;
            if (msg[8] == 1) {
                // Login details are not valid. Reject.
                byte z = 0;
                byte[] msg1 = { (byte) 'N', (byte) 'T', (byte) 'L',
                        (byte) 'M', (byte) 'S', (byte) 'S', (byte) 'P',
                        z, (byte) 2, z, z, z, z, z, z, z, (byte) 40, z,
                        z, z, (byte) 1, (byte) 130, z, z, z, (byte) 2,
                        (byte) 2, (byte) 2, z, z, z, z, z, z, z, z, z,
                        z, z, z };
                res.setHeader(
                        "WWW-Authenticate", 
                        "NTLM " + Base64.encodeBase64String(msg1));
                res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                res.setContentLength(0);
                res.flushBuffer();
                return;
            } else if (msg[8] == 3) {
                // Login details seem valid. Grab the username.
                off = 30;

                length = msg[off + 9] * 256 + msg[off + 8];
                offset = msg[off + 11] * 256 + msg[off + 10];
                username = new String(msg, offset, length);
                username = canonicalUsername(username);
            }
        }

        req.getSession().setAttribute(USERNAME_KEY, username);

        setRequestHeaders(req);

        log.info("User details now stored in session: " + username);

        proceed(req, res, chain);
    }

    private void setRequestHeaders(HttpServletRequest req) {
        req.setAttribute(USERNAME_KEY, req.getSession().getAttribute(USERNAME_KEY));
    }

    /**
     * To avoid issues with comparing user names with differing case and spaces, 
     * this method strips out extraneous spaces and lower-cases it.
     */
    private String canonicalUsername(String username) {
        return username.replaceAll("[^a-zA-Z0-9#]", "").toLowerCase().trim();
    }

}

You may notice that this creates an SM_USER header in the request. If you make sure that this filter runs before a RequestHeaderAuthenticationFilter, then you have a nice setup where the header is defined by the SSO filter and then everything is passed on to standard Spring authentication processing. This can be done like so...

@Configuration
@EnableWebSecurity
@EnableWebMvcSecurity
@Profile("secure")
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired(required = true)
    @Qualifier("ntlmFilter")
    private Filter ntlmFilter;

    @Autowired(required = true)
    @Qualifier("headerAuthFilter")
    private Filter headerAuthFilter;

    // ...

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(ntlmFilter, RequestHeaderAuthenticationFilter.class)
            .anonymous().disable()
            .csrf().disable()
            .exceptionHandling().authenticationEntryPoint(http403ForbiddenEntryPoint());
    }

    @Bean(name = "headerAuthFilter")
    public Filter headerAuthFilter(AuthenticationManager authenticationManager) {
        RequestHeaderAuthenticationFilter filter = new RequestHeaderAuthenticationFilter();
        filter.setPrincipalRequestHeader("SM_USER");
        filter.setAuthenticationManager(authenticationManager);
        filter.setExceptionIfHeaderMissing(false);
        return filter;
    }

    // ...

}

Upvotes: 2

seenukarthi
seenukarthi

Reputation: 8674

What you have configured is LDAP authentication if you want do automatic authentication you have to use Kerberos/SPNEGO.

Spring has a module for Kerberos/SPNEGO authentication check this blog which explains how Kerberos/SPNEGO works and how to configure spring security.

Also you have to enable "Windows Integerated Authentication" in IE as follows.

enter image description here

Upvotes: 0

Related Questions