سؤال

Can they work together? Some project sample would be great.

I have a web-app on Spring3. And i need to implement NTLM. Spring stopped NTLM support in 3rd version. Is there any possibilities to implement it?

Looking for a sample project.

هل كانت مفيدة؟

المحلول

They can be used together. Essentially what you want to do is hook into the SPNEGO protocol and detect when you receive an NTLM packet from the client. A good description of the protocol can be found here:

http://www.innovation.ch/personal/ronald/ntlm.html

http://blogs.technet.com/b/tristank/archive/2006/08/02/negotiate-this.aspx

Another great resource for NTLM is this:

http://davenport.sourceforge.net/ntlm.html

But you asked for a sample so here goes. To detect an NTLM packet you need to base64 decode the packet and inspect for a starting string:

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

    String header = request.getHeader("Authorization");

    if ((header != null) && header.startsWith("Negotiate ")) {
        if (logger.isDebugEnabled()) {
            logger.debug("Received Negotiate Header for request " + request.getRequestURL() + ": " + header);
        }
        byte[] base64Token = header.substring(10).getBytes("UTF-8");
        byte[] decodedToken = Base64.decode(base64Token);

    if (isNTLMMessage(decodedToken)) {
        authenticationRequest = new NTLMServiceRequestToken(decodedToken);
    }

...
}

public static boolean isNTLMMessage(byte[] token) {
    for (int i = 0; i < 8; i++) {
        if (token[i] != NTLMSSP_SIGNATURE[i]) {
            return false;
        }
    }
    return true;
}

public static final byte[] NTLMSSP_SIGNATURE = new byte[]{
        (byte) 'N', (byte) 'T', (byte) 'L', (byte) 'M',
        (byte) 'S', (byte) 'S', (byte) 'P', (byte) 0
};

You'll need to make an authentication provider that can handle that type of authenticationRequest:

import jcifs.Config;
import jcifs.UniAddress;
import jcifs.ntlmssp.NtlmMessage;
import jcifs.ntlmssp.Type1Message;
import jcifs.ntlmssp.Type2Message;
import jcifs.ntlmssp.Type3Message;
import jcifs.smb.NtlmPasswordAuthentication;
import jcifs.smb.SmbSession;
import jcifs.util.Base64;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AccountStatusUserDetailsChecker;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsChecker;

import javax.annotation.PostConstruct;
import java.io.IOException;

/**
 * User: gcermak
 * Date: 3/15/11
 * <p/>
 */
public class ActiveDirectoryNTLMAuthenticationProvider implements AuthenticationProvider, InitializingBean {
    protected String defaultDomain;
    protected String domainController;

    protected UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();

    public ActiveDirectoryNTLMAuthenticationProvider(){
        Config.setProperty( "jcifs.smb.client.soTimeout", "1800000" );
        Config.setProperty( "jcifs.netbios.cachePolicy", "1200" );
        Config.setProperty( "jcifs.smb.lmCompatibility", "0" );
        Config.setProperty( "jcifs.smb.client.useExtendedSecurity", "false" );
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        NTLMServiceRequestToken auth = (NTLMServiceRequestToken) authentication;
        byte[] token = auth.getToken();

        String name = null;
        String password = null;

        NtlmMessage message = constructNTLMMessage(token);

        if (message instanceof Type1Message) {
            Type2Message type2msg = null;
            try {
                type2msg = new Type2Message(new Type1Message(token), getChallenge(), null);
                throw new NtlmType2MessageException(Base64.encode(type2msg.toByteArray()));
            } catch (IOException e) {
                throw new NtlmAuthenticationFailure(e.getMessage());
            }
        }
        if (message instanceof Type3Message) {
            final Type3Message type3msg;
            try {
                type3msg = new Type3Message(token);
            } catch (IOException e) {
                throw new NtlmAuthenticationFailure(e.getMessage());
            }
            final byte[] lmResponse = (type3msg.getLMResponse() != null) ? type3msg.getLMResponse() : new byte[0];
            final byte[] ntResponse = (type3msg.getNTResponse() != null) ? type3msg.getNTResponse() : new byte[0];

            NtlmPasswordAuthentication ntlmPasswordAuthentication = new NtlmPasswordAuthentication(type3msg.getDomain(), type3msg.getUser(), getChallenge(), lmResponse, ntResponse);

            String username = ntlmPasswordAuthentication.getUsername();
            String domain = ntlmPasswordAuthentication.getDomain();
            String workstation = type3msg.getWorkstation();

            name = ntlmPasswordAuthentication.getName();
            password = ntlmPasswordAuthentication.getPassword();
        }

        // do custom logic here to find the user ... 
        userDetailsChecker.check(user);

        return new UsernamePasswordAuthenticationToken(user, password, user.getAuthorities());
    }

    // The Client will only ever send a Type1 or Type3 message ... try 'em both
    protected static NtlmMessage constructNTLMMessage(byte[] token) {
        NtlmMessage message = null;
        try {
            message = new Type1Message(token);
            return message;
        } catch (IOException e) {
            if ("Not an NTLMSSP message.".equals(e.getMessage())) {
                return null;
            }
        }

        try {
            message = new Type3Message(token);
            return message;
        } catch (IOException e) {
            if ("Not an NTLMSSP message.".equals(e.getMessage())) {
                return null;
            }
        }

        return message;
    }

    protected byte[] getChallenge() {
        UniAddress dcAddress = null;
        try {
            dcAddress = UniAddress.getByName(domainController, true);
            return SmbSession.getChallenge(dcAddress);
        } catch (IOException e) {
            throw new NtlmAuthenticationFailure(e.getMessage());
        }
    }

    @Override
    public boolean supports(Class<? extends Object> auth) {
        return NTLMServiceRequestToken.class.isAssignableFrom(auth);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        // do nothing
    }

    public void setSmbClientUsername(String smbClientUsername) {
        Config.setProperty("jcifs.smb.client.username", smbClientUsername);
    }

    public void setSmbClientPassword(String smbClientPassword) {
        Config.setProperty("jcifs.smb.client.password", smbClientPassword);
    }

    public void setDefaultDomain(String defaultDomain) {
        this.defaultDomain = defaultDomain;
        Config.setProperty("jcifs.smb.client.domain", defaultDomain);
    }

    /**
     * 0: Nothing
     * 1: Critical [default]
     * 2: Basic info. (Can be logged under load)
     * 3: Detailed info. (Highest recommended level for production use)
     * 4: Individual smb messages
     * 6: Hex dumps
     * @param logLevel  the desired logging level
     */
    public void setDebugLevel(int logLevel) throws Exception {
        switch(logLevel) {
            case 0:
            case 1:
            case 2:
            case 3:
            case 4:
            case 6:
                Config.setProperty("jcifs.util.loglevel", Integer.toString(logLevel));
                break;
            default:
                throw new Exception("Invalid Log Level specified");
        }
    }

    /**
     *
     * @param winsList a comma separates list of wins addresses (ex. 10.169.10.77,10.169.10.66)
     */
    public void setNetBiosWins(String winsList) {
        Config.setProperty("jcifs.netbios.wins", winsList);
    }

    public void setDomainController(String domainController) {
        this.domainController = domainController;
    }
}

And finally you need to tie it all together in your spring_security.xml file:

<beans:beans xmlns="http://www.springframework.org/schema/security"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xmlns:beans="http://www.springframework.org/schema/beans"
             xmlns:jdbc="http://www.springframework.org/schema/jdbc"
             xsi:schemaLocation="
                http://www.springframework.org/schema/beans
                http://www.springframework.org/schema/beans/spring-beans.xsd
                http://www.springframework.org/schema/jdbc
                http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd
                http://www.springframework.org/schema/security
                http://www.springframework.org/schema/security/spring-security-3.1.xsd">

    <http auto-config="true" use-expressions="true" disable-url-rewriting="true">
        <form-login login-page="/auth/login"
                    login-processing-url="/auth/j_security_check"/>
        <remember-me services-ref="rememberMeServices"/>
        <logout invalidate-session="true" logout-success-url="/auth/logoutMessage" logout-url="/auth/logout"/>
        <access-denied-handler error-page="/error/accessDenied"/>
    </http>

    <authentication-manager alias="authenticationManager">
        <authentication-provider user-service-ref="myUsernamePasswordUserDetailsService">
            <password-encoder ref="passwordEncoder">
                <salt-source ref="saltSource"/>
            </password-encoder>
        </authentication-provider>
        <authentication-provider ref="NTLMAuthenticationProvider"/>
    </authentication-manager>
</beans:beans>

Lastly you need to know how to tie it all together. The protocol as described in the first set of links shows that there are a couple round trips that you need to make between the client and server. Thus in your filter you need a bit more logic:

import jcifs.ntlmssp.Type1Message;
import jcifs.ntlmssp.Type2Message;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.codec.Base64;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.extensions.kerberos.KerberosServiceRequestToken;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.util.Assert;
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;

/**
 * User: gcermak
 * Date: 12/5/11
 */
public class SpnegoAuthenticationProcessingFilter extends GenericFilterBean {
    private AuthenticationManager authenticationManager;
    private AuthenticationSuccessHandler successHandler;
    private AuthenticationFailureHandler failureHandler;

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

        String header = request.getHeader("Authorization");

        if ((header != null) && header.startsWith("Negotiate ")) {
            if (logger.isDebugEnabled()) {
                logger.debug("Received Negotiate Header for request " + request.getRequestURL() + ": " + header);
            }
            byte[] base64Token = header.substring(10).getBytes("UTF-8");
            byte[] decodedToken = Base64.decode(base64Token);

            // older versions of ie will sometimes do this
            // logic cribbed from jcifs filter implementation jcifs.http.NtlmHttpFilter
            if (request.getMethod().equalsIgnoreCase("POST")) {
                if (decodedToken[8] == 1) {
                    logger.debug("NTLM Authorization header contains type-1 message. Sending fake response just to pass this stage...");
                    Type1Message type1 = new Type1Message(decodedToken);
                    // respond with a type 2 message, where the challenge is null since we don't
                    // care about the server response (type-3 message) since we're already authenticated
                    // (This is just a by-pass - see method javadoc)
                    Type2Message type2 = new Type2Message(type1, new byte[8], null);
                    String msg = jcifs.util.Base64.encode(type2.toByteArray());
                    response.setHeader("WWW-Authenticate", "Negotiate " + msg);
                    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                    response.setContentLength(0);
                    response.flushBuffer();
                    return;
                }
            }

            Authentication authenticationRequest = null;
            if (isNTLMMessage(decodedToken)) {
                authenticationRequest = new NTLMServiceRequestToken(decodedToken);
            } 

            Authentication authentication;
            try {
                authentication = authenticationManager.authenticate(authenticationRequest);
            } catch (NtlmBaseException e) {
                // this happens during the normal course of action of an NTLM authentication
                // a type 2 message is the proper response to a type 1 message from the client
                // see: http://www.innovation.ch/personal/ronald/ntlm.html
                response.setHeader("WWW-Authenticate", e.getMessage());
                response.setHeader("Connection", "Keep-Alive");
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.setContentLength(0);
                response.flushBuffer();
                return;
            } catch (AuthenticationException e) {
                // That shouldn't happen, as it is most likely a wrong configuration on the server side
                logger.warn("Negotiate Header was invalid: " + header, e);
                SecurityContextHolder.clearContext();
                if (failureHandler != null) {
                    failureHandler.onAuthenticationFailure(request, response, e);
                } else {
                    response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                    response.flushBuffer();
                }
                return;
            }
            if (successHandler != null) {
                successHandler.onAuthenticationSuccess(request, response, authentication);
            }
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        chain.doFilter(request, response);
    }

    public void setAuthenticationManager(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    public void setSuccessHandler(AuthenticationSuccessHandler successHandler) {
        this.successHandler = successHandler;
    }

    public void setFailureHandler(AuthenticationFailureHandler failureHandler) {
        this.failureHandler = failureHandler;
    }

    @Override
    public void afterPropertiesSet() throws ServletException {
        super.afterPropertiesSet();
        Assert.notNull(this.authenticationManager, "authenticationManager must be specified");
    }
}

You'll see that in the exception we use "Negotiate" rather than NTLM:

/**
 * User: gcermak
 * Date: 12/5/11
 */
public class NtlmType2MessageException extends NtlmBaseException {
    private static final long serialVersionUID = 1L;

    public NtlmType2MessageException(final String type2Msg) {
        super("Negotiate " + type2Msg);
    }
}

The spring filter (above) was largely patterned on jcifs.http.NtlmHttpFilter which you can find in the source for jcifs here:

http://jcifs.samba.org/

This isn't a whole, downloadable project as you requested but if there is interest from the community I could add this NTLM code to my github project:

http://git.springsource.org/~grantcermak/spring-security/activedirectory-se-security

Hope this helps!

Grant

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top