diff --git a/ntlm/README b/ntlm/README
new file mode 100755
index 0000000000..5727d3ede1
--- /dev/null
+++ b/ntlm/README
@@ -0,0 +1,5 @@
+Just place this folder into the SVN checkout of ACEGI sources.
+Then modify the root pom.xml to include the folder as a module.
+
+The applicationContext.xml and web.xml files are included in
+the root directory for example purposes only.
\ No newline at end of file
diff --git a/ntlm/applicationContext.xml b/ntlm/applicationContext.xml
new file mode 100755
index 0000000000..c32db1ac7b
--- /dev/null
+++ b/ntlm/applicationContext.xml
@@ -0,0 +1,95 @@
+
+
+
+AuthenticationException
s in the
+ * {@link NtlmProcessingFilterEntryPoint}. Marked as abstract
+ * since this exception is never supposed to be instantiated.
+ *
+ * @author Edward Smith
+ */
+public abstract class NtlmBaseException extends AuthenticationException {
+
+ public NtlmBaseException(final String msg) {
+ super(msg);
+ }
+
+} // End NtlmBaseException
diff --git a/ntlm/src/main/java/org/acegisecurity/ui/ntlm/NtlmBeginHandshakeException.java b/ntlm/src/main/java/org/acegisecurity/ui/ntlm/NtlmBeginHandshakeException.java
new file mode 100755
index 0000000000..7ec54e2937
--- /dev/null
+++ b/ntlm/src/main/java/org/acegisecurity/ui/ntlm/NtlmBeginHandshakeException.java
@@ -0,0 +1,31 @@
+/* Copyright 2004-2007 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.acegisecurity.ui.ntlm;
+
+import org.acegisecurity.AuthenticationException;
+
+/**
+ * Signals the beginning of an NTLM handshaking process.
+ *
+ * @author Edward Smith
+ */
+public class NtlmBeginHandshakeException extends NtlmBaseException {
+
+ public NtlmBeginHandshakeException() {
+ super("NTLM");
+ }
+
+} // End NtlmBeginHandshakeException
diff --git a/ntlm/src/main/java/org/acegisecurity/ui/ntlm/NtlmProcessingFilter.java b/ntlm/src/main/java/org/acegisecurity/ui/ntlm/NtlmProcessingFilter.java
new file mode 100755
index 0000000000..b9f6102ade
--- /dev/null
+++ b/ntlm/src/main/java/org/acegisecurity/ui/ntlm/NtlmProcessingFilter.java
@@ -0,0 +1,511 @@
+/* Copyright 2004-2007 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.acegisecurity.ui.ntlm;
+
+import java.io.IOException;
+import java.net.UnknownHostException;
+import java.util.Enumeration;
+import java.util.Properties;
+
+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 javax.servlet.http.HttpSession;
+
+import jcifs.Config;
+import jcifs.UniAddress;
+import jcifs.ntlmssp.Type1Message;
+import jcifs.ntlmssp.Type2Message;
+import jcifs.ntlmssp.Type3Message;
+import jcifs.smb.NtlmChallenge;
+import jcifs.smb.NtlmPasswordAuthentication;
+import jcifs.smb.SmbAuthException;
+import jcifs.smb.SmbException;
+import jcifs.smb.SmbSession;
+import jcifs.util.Base64;
+
+import org.acegisecurity.AcegiMessageSource;
+import org.acegisecurity.Authentication;
+import org.acegisecurity.AuthenticationCredentialsNotFoundException;
+import org.acegisecurity.AuthenticationException;
+import org.acegisecurity.AuthenticationManager;
+import org.acegisecurity.BadCredentialsException;
+import org.acegisecurity.InsufficientAuthenticationException;
+import org.acegisecurity.context.SecurityContextHolder;
+import org.acegisecurity.event.authentication.InteractiveAuthenticationSuccessEvent;
+import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
+import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken;
+import org.acegisecurity.ui.AbstractProcessingFilter;
+import org.acegisecurity.ui.WebAuthenticationDetails;
+import org.acegisecurity.ui.savedrequest.SavedRequest;
+import org.acegisecurity.ui.webapp.AuthenticationProcessingFilter;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.context.support.MessageSourceAccessor;
+import org.springframework.util.Assert;
+
+/**
+ * A clean-room implementation for Acegi Security System of an NTLM HTTP filter
+ * leveraging the JCIFS library.
+ *
+ * NTLM is a Microsoft-developed protocol providing single sign-on capabilities + * to web applications and other integrated applications. It allows a web + * server to automatcially discover the username of a browser client when that + * client is logged into a Windows domain and is using an NTLM-aware browser. + * A web application can then reuse the user's Windows credentials without + * having to ask for them again. + *
+ * Because NTLM only provides the username of the Windows client, an Acegi
+ * Security NTLM deployment must have a UserDetailsService
that
+ * provides a UserDetails
object with the empty string as the
+ * password and whatever GrantedAuthority
values necessary to
+ * pass the FilterSecurityInterceptor
.
+ *
+ * The Acegi Security bean configuration file must also place the
+ * ExceptionTranslationFilter
before this filter in the
+ * FilterChainProxy
definition.
+ *
+ * @author Davide Baroncelli
+ * @author Edward Smith
+ * @version $Id$
+ */
+public class NtlmProcessingFilter extends HttpFilter implements InitializingBean {
+ //~ Static fields/initializers =====================================================================================
+
+ private static Log logger = LogFactory.getLog(NtlmProcessingFilter.class);
+
+ private static final String STATE_ATTR = "AcegiNtlm";
+ private static final String CHALLENGE_ATTR = "NtlmChal";
+ private static final Integer BEGIN = Integer.valueOf(0);
+ private static final Integer NEGOTIATE = Integer.valueOf(1);
+ private static final Integer COMPLETE = Integer.valueOf(2);
+ private static final Integer DELAYED = Integer.valueOf(3);
+
+ //~ Instance fields ================================================================================================
+
+ /** Shoud the filter load balance among multiple domain controllers, default false
*/
+ private boolean loadBalance;
+
+ /** Shoud the domain name be stripped from the username, default true
*/
+ private boolean stripDomain = true;
+
+ /** Should the filter initiate NTLM negotiations, default true
*/
+ private boolean forceIdentification = true;
+
+ /** Shoud the filter retry NTLM on authorization failure, default false
*/
+ private boolean retryOnAuthFailure;
+
+ private String soTimeout;
+ private String cachePolicy;
+ private String defaultDomain;
+ private String domainController;
+ private AuthenticationManager authenticationManager;
+
+ //~ Public Methods =================================================================================================
+
+ /**
+ * Ensures an AuthenticationManager
and authentication failure
+ * URL have been provided in the bean configuration file.
+ */
+ public void afterPropertiesSet() throws Exception {
+ Assert.notNull(this.authenticationManager, "An AuthenticationManager is required");
+
+ // Default to 5 minutes if not already specified
+ Config.setProperty("jcifs.smb.client.soTimeout", (soTimeout == null) ? "300000" : soTimeout);
+ // Default to 20 minutes if not already specified
+ Config.setProperty("jcifs.netbios.cachePolicy", (cachePolicy == null) ? "1200" : cachePolicy);
+
+ if (domainController == null) {
+ domainController = defaultDomain;
+ }
+ }
+
+ /**
+ * Sets the AuthenticationManager
to use.
+ *
+ * @param authenticationManager the AuthenticationManager
to use.
+ */
+ public void setAuthenticationManager(AuthenticationManager authenticationManager) {
+ this.authenticationManager = authenticationManager;
+ }
+
+ /**
+ * The NT domain against which clients should be authenticated. If the SMB
+ * client username and password are also set, then preauthentication will
+ * be used which is necessary to initialize the SMB signing digest. SMB
+ * signatures are required by default on Windows 2003 domain controllers.
+ *
+ * @param defaultDomain The name of the default domain.
+ */
+ public void setDefaultDomain(String defaultDomain) {
+ this.defaultDomain = defaultDomain;
+ Config.setProperty("jcifs.smb.client.domain", defaultDomain);
+ }
+
+ /**
+ * Sets the SMB client username.
+ *
+ * @param smbClientUsername The SMB client username.
+ */
+ public void setSmbClientUsername(String smbClientUsername) {
+ Config.setProperty("jcifs.smb.client.username", smbClientUsername);
+ }
+
+ /**
+ * Sets the SMB client password.
+ *
+ * @param smbClientPassword The SMB client password.
+ */
+ public void setSmbClientPassword(String smbClientPassword) {
+ Config.setProperty("jcifs.smb.client.password", smbClientPassword);
+ }
+
+ /**
+ * Sets the SMB client SSN limit. When set to 1
, every
+ * authentication is forced to use a separate transport. This effectively
+ * ignores SMB signing requirements, however at the expense of reducing
+ * scalability. Preauthentication with a domain, username, and password is
+ * the preferred method for working with servers that require signatures.
+ *
+ * @param smbClientSSNLimit The SMB client SSN limit.
+ */
+ public void setSmbClientSSNLimit(String smbClientSSNLimit) {
+ Config.setProperty("jcifs.smb.client.ssnLimit", smbClientSSNLimit);
+ }
+
+ /**
+ * Configures JCIFS to use a WINS server. It is preferred to use a WINS
+ * server over a specific domain controller. Set this property instead of
+ * domainController
if there is a WINS server available.
+ *
+ * @param netbiosWINS The WINS server JCIFS will use.
+ */
+ public void setNetbiosWINS(String netbiosWINS) {
+ Config.setProperty("jcifs.netbios.wins", netbiosWINS);
+ }
+
+ /**
+ * The IP address of any SMB server that should be used to authenticate
+ * HTTP clients.
+ *
+ * @param domainController The IP address of the domain controller.
+ */
+ public void setDomainController(String domainController) {
+ this.domainController = domainController;
+ }
+
+ /**
+ * If the default domain is specified and the domain controller is not
+ * specified, then query for domain controllers by name. When load
+ * balance is true
, rotate through the list of domain
+ * controllers when authenticating users.
+ *
+ * @param loadBalance The load balance flag value.
+ */
+ public void setLoadBalance(boolean loadBalance) {
+ this.loadBalance = loadBalance;
+ }
+
+ /**
+ * Configures NtlmProcessingFilter
to strip the Windows
+ * domain name from the username when set to true
, which
+ * is the default value.
+ *
+ * @param stripDomain The strip domain flag value.
+ */
+ public void setStripDomain(boolean stripDomain) {
+ this.stripDomain = stripDomain;
+ }
+
+ /**
+ * Sets the jcifs.smb.client.soTimeout
property to the
+ * timeout value specified in milliseconds. Defaults to 5 minutes
+ * if not specified.
+ *
+ * @param timeout The milliseconds timeout value.
+ */
+ public void setSoTimeout(String timeout) {
+ this.soTimeout = timeout;
+ }
+
+ /**
+ * Sets the jcifs.netbios.cachePolicy
property to the
+ * number of seconds a NetBIOS address is cached by JCIFS. Defaults to
+ * 20 minutes if not specified.
+ *
+ * @param numSeconds The number of seconds a NetBIOS address is cached.
+ */
+ public void setCachePolicy(String numSeconds) {
+ this.cachePolicy = numSeconds;
+ }
+
+ /**
+ * Loads properties starting with "jcifs" into the JCIFS configuration.
+ * Any other properties are ignored.
+ *
+ * @param props The JCIFS properties to set.
+ */
+ public void setJcifsProperties(Properties props) {
+ String name;
+
+ for (Enumeration e=props.keys(); e.hasMoreElements();) {
+ name = (String) e.nextElement();
+ if (name.startsWith("jcifs.")) {
+ Config.setProperty(name, props.getProperty(name));
+ }
+ }
+ }
+
+ /**
+ * Returns true
if NTLM authentication is forced.
+ *
+ * @return true
if NTLM authentication is forced.
+ */
+ public boolean isForceIdentification() {
+ return this.forceIdentification;
+ }
+
+ /**
+ * Sets a flag denoting whether NTLM authentication should be forced.
+ *
+ * @param forceIdentification the force identification flag value to set.
+ */
+ public void setForceIdentification(boolean forceIdentification) {
+ this.forceIdentification = forceIdentification;
+ }
+
+ /**
+ * Sets a flag denoting whether NTLM should retry whenever authentication
+ * fails. Retry will only occur on an {@link AuthenticationCredentialsNotFoundException}
+ * or {@link InsufficientAuthenticationException}.
+ *
+ * @param retryOnFailure the retry on failure flag value to set.
+ */
+ public void setRetryOnAuthFailure(boolean retryOnFailure) {
+ this.retryOnAuthFailure = retryOnFailure;
+ }
+
+ //~ Protected Methods ==============================================================================================
+
+ protected void doFilter(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) throws IOException {
+ final HttpSession session = request.getSession();
+ Integer ntlmState = (Integer) session.getAttribute(STATE_ATTR);
+
+ // Start NTLM negotiations the first time through the filter
+ if (ntlmState == null) {
+ if (forceIdentification) {
+ logger.debug("Starting NTLM handshake");
+ session.setAttribute(STATE_ATTR, BEGIN);
+ throw new NtlmBeginHandshakeException();
+ } else {
+ logger.debug("NTLM handshake not yet started");
+ session.setAttribute(STATE_ATTR, DELAYED);
+ }
+ }
+
+ // IE will send a Type 1 message to reauthenticate the user during an HTTP POST
+ if (ntlmState == COMPLETE && this.reAuthOnIEPost(request))
+ ntlmState = BEGIN;
+
+ final String authMessage = request.getHeader("Authorization");
+ if (ntlmState != COMPLETE && authMessage != null && authMessage.startsWith("NTLM ")) {
+ final UniAddress dcAddress = this.getDCAddress(session);
+ if (ntlmState == BEGIN) {
+ logger.debug("Processing NTLM Type 1 Message");
+ session.setAttribute(STATE_ATTR, NEGOTIATE);
+ this.processType1Message(authMessage, session, dcAddress);
+ } else {
+ logger.debug("Processing NTLM Type 3 Message");
+ final NtlmPasswordAuthentication auth = this.processType3Message(authMessage, session, dcAddress);
+ logger.debug("NTLM negotiation complete");
+ this.logon(session, dcAddress, auth);
+ session.setAttribute(STATE_ATTR, COMPLETE);
+
+ // Do not reauthenticate the user in Acegi during an IE POST
+ final Authentication myCurrentAuth = SecurityContextHolder.getContext().getAuthentication();
+ if (myCurrentAuth == null || myCurrentAuth instanceof AnonymousAuthenticationToken) {
+ logger.debug("Authenticating user credentials");
+ this.authenticate(request, response, session, auth);
+ }
+ }
+ }
+ }
+
+ //~ Private Methods ================================================================================================
+
+ /**
+ * Returns true
if reauthentication is needed on an IE POST.
+ */
+ private boolean reAuthOnIEPost(final HttpServletRequest request) {
+ String ua = request.getHeader("User-Agent");
+ return (request.getMethod().equalsIgnoreCase("POST") && ua != null && ua.indexOf("MSIE") != -1);
+ }
+
+ /**
+ * Creates and returns a Type 2 message from the provided Type 1 message.
+ *
+ * @param message the Type 1 message to process.
+ * @param session the HTTPSession
object.
+ * @param dcAddress the domain controller address.
+ * @throws IOException
+ */
+ private void processType1Message(final String message, final HttpSession session, final UniAddress dcAddress) throws IOException {
+ final Type2Message type2msg = new Type2Message(
+ new Type1Message(Base64.decode(message.substring(5))),
+ this.getChallenge(session, dcAddress),
+ null);
+ throw new NtlmType2MessageException(Base64.encode(type2msg.toByteArray()));
+ }
+
+ /**
+ * Builds and returns an NtlmPasswordAuthentication
object
+ * from the provided Type 3 message.
+ *
+ * @param message the Type 3 message to process.
+ * @param session the HTTPSession
object.
+ * @param dcAddress the domain controller address.
+ * @return an NtlmPasswordAuthentication
object.
+ * @throws IOException
+ */
+ private NtlmPasswordAuthentication processType3Message(final String message, final HttpSession session, final UniAddress dcAddress) throws IOException {
+ final Type3Message type3msg = new Type3Message(Base64.decode(message.substring(5)));
+ final byte[] lmResponse = (type3msg.getLMResponse() != null) ? type3msg.getLMResponse() : new byte[0];
+ final byte[] ntResponse = (type3msg.getNTResponse() != null) ? type3msg.getNTResponse() : new byte[0];
+ return new NtlmPasswordAuthentication(
+ type3msg.getDomain(),
+ type3msg.getUser(),
+ this.getChallenge(session, dcAddress),
+ lmResponse,
+ ntResponse);
+ }
+
+ /**
+ * Checks the user credentials against the domain controller.
+ *
+ * @param session the HTTPSession
object.
+ * @param dcAddress the domain controller address.
+ * @param auth the NtlmPasswordAuthentication
object.
+ * @throws IOException
+ */
+ private void logon(final HttpSession session, final UniAddress dcAddress, final NtlmPasswordAuthentication auth) throws IOException {
+ try {
+ SmbSession.logon(dcAddress, auth);
+ if (logger.isDebugEnabled()) {
+ logger.debug(auth + " successfully authenticated against " + dcAddress);
+ }
+ } catch(SmbAuthException e) {
+ logger.error("Credentials " + auth + " were not accepted by the domain controller " + dcAddress);
+ throw new BadCredentialsException("Bad NTLM credentials");
+ } finally {
+ if (loadBalance)
+ session.removeAttribute(CHALLENGE_ATTR);
+ }
+ }
+
+ /**
+ * Authenticates the user credentials acquired from NTLM against the Acegi
+ * Security AuthenticationManager
.
+ *
+ * @param request the HttpServletRequest
object.
+ * @param response the HttpServletResponse
object.
+ * @param session the HttpSession
object.
+ * @param auth the NtlmPasswordAuthentication
object.
+ * @throws IOException
+ */
+ private void authenticate(final HttpServletRequest request, final HttpServletResponse response, final HttpSession session, final NtlmPasswordAuthentication auth) throws IOException {
+ final Authentication authResult;
+ final UsernamePasswordAuthenticationToken authRequest;
+ final Authentication backupAuth;
+
+ authRequest = new NtlmUsernamePasswordAuthenticationToken(auth, stripDomain);
+ authRequest.setDetails(new WebAuthenticationDetails(request));
+
+ // Place the last username attempted into HttpSession for views
+ session.setAttribute(AuthenticationProcessingFilter.ACEGI_SECURITY_LAST_USERNAME_KEY, authRequest.getName());
+
+ // Backup the current authentication in case of an AuthenticationException
+ backupAuth = SecurityContextHolder.getContext().getAuthentication();
+
+ try {
+ // Authenitcate the user with the authentication manager
+ authResult = authenticationManager.authenticate(authRequest);
+ } catch (AuthenticationException failed) {
+ if (logger.isInfoEnabled()) {
+ logger.info("Authentication request for user: " + authRequest.getName() + " failed: " + failed.toString());
+ }
+
+ // Reset the backup Authentication object and rethrow the AuthenticationException
+ SecurityContextHolder.getContext().setAuthentication(backupAuth);
+
+ if (retryOnAuthFailure && (failed instanceof AuthenticationCredentialsNotFoundException || failed instanceof InsufficientAuthenticationException)) {
+ logger.debug("Restart NTLM authentication handshake due to AuthenticationException");
+ session.setAttribute(STATE_ATTR, BEGIN);
+ throw new NtlmBeginHandshakeException();
+ }
+
+ throw failed;
+ }
+
+ // Set the Authentication object with the valid authentication result
+ SecurityContextHolder.getContext().setAuthentication(authResult);
+ }
+
+ /**
+ * Returns the domain controller address based on the loadBalance
+ * setting.
+ *
+ * @param session the HttpSession
object.
+ * @return the domain controller address.
+ * @throws UnknownHostException
+ * @throws SmbException
+ */
+ private UniAddress getDCAddress(final HttpSession session) throws UnknownHostException, SmbException {
+ if (loadBalance) {
+ NtlmChallenge chal = (NtlmChallenge) session.getAttribute(CHALLENGE_ATTR);
+ if (chal == null) {
+ chal = SmbSession.getChallengeForDomain();
+ session.setAttribute(CHALLENGE_ATTR, chal);
+ }
+ return chal.dc;
+ }
+
+ return UniAddress.getByName(domainController, true);
+ }
+
+ /**
+ * Returns the domain controller challenge based on the loadBalance
+ * setting.
+ *
+ * @param session the HttpSession
object.
+ * @param dcAddress the domain controller address.
+ * @return the domain controller challenge.
+ * @throws UnknownHostException
+ * @throws SmbException
+ */
+ private byte[] getChallenge(final HttpSession session, final UniAddress dcAddress) throws UnknownHostException, SmbException {
+ if (loadBalance)
+ return ((NtlmChallenge) session.getAttribute(CHALLENGE_ATTR)).challenge;
+
+ return SmbSession.getChallenge(dcAddress);
+ }
+
+} // End NtlmProcessingFilter
diff --git a/ntlm/src/main/java/org/acegisecurity/ui/ntlm/NtlmProcessingFilterEntryPoint.java b/ntlm/src/main/java/org/acegisecurity/ui/ntlm/NtlmProcessingFilterEntryPoint.java
new file mode 100755
index 0000000000..2d852a7e97
--- /dev/null
+++ b/ntlm/src/main/java/org/acegisecurity/ui/ntlm/NtlmProcessingFilterEntryPoint.java
@@ -0,0 +1,119 @@
+/* Copyright 2004-2007 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.acegisecurity.ui.ntlm;
+
+import org.acegisecurity.AuthenticationCredentialsNotFoundException;
+import org.acegisecurity.AuthenticationException;
+import org.acegisecurity.InsufficientAuthenticationException;
+import org.acegisecurity.ui.AuthenticationEntryPoint;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.util.Assert;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import jcifs.Config;
+
+/**
+ * Used by ExceptionTranslationFilter
to assist with the NTLM
+ * negotiation. Also handles redirecting the user to the authentication
+ * failure URL if an {@link AuthenticationException} that is not a subclass of
+ * {@link NtlmBaseException} is received.
+ *
+ * @author Davide Baroncelli
+ * @author Edward Smith
+ * @version $Id$
+ */
+public class NtlmProcessingFilterEntryPoint implements AuthenticationEntryPoint, InitializingBean {
+ //~ Static fields/initializers =============================================
+
+ private static final Log logger = LogFactory.getLog(NtlmProcessingFilterEntryPoint.class);
+
+ //~ Instance fields ================================================================================================
+
+ /** Where to redirect the browser to if authentication fails */
+ private String authenticationFailureUrl;
+
+ //~ Methods ================================================================
+
+ /**
+ * Ensures an authentication failure URL has been provided in the bean
+ * configuration file.
+ */
+ public void afterPropertiesSet() throws Exception {
+ Assert.hasLength(authenticationFailureUrl, "authenticationFailureUrl must be specified");
+ }
+
+ /**
+ * Sets the authentication failure URL.
+ *
+ * @param authenticationFailureUrl the authentication failure URL.
+ */
+ public void setAuthenticationFailureUrl(String authenticationFailureUrl) {
+ this.authenticationFailureUrl = authenticationFailureUrl;
+ }
+
+ /**
+ * Sends an NTLM challenge to the browser requiring authentication. The
+ * WWW-Authenticate header is populated with the appropriate information
+ * during the negotiation lifecycle by calling the getMessage() method
+ * from an NTLM-specific subclass of {@link NtlmBaseException}:
+ *
+ *
Authentication
object each time
+ * Internet Explorer does a POST.
+ */
+ public void preserveAuthentication() {
+ if (auth != null)
+ SecurityContextHolder.getContext().setAuthentication(auth);
+ }
+
+} // End NTLMType2MessageException
diff --git a/ntlm/src/main/java/org/acegisecurity/ui/ntlm/NtlmUsernamePasswordAuthenticationToken.java b/ntlm/src/main/java/org/acegisecurity/ui/ntlm/NtlmUsernamePasswordAuthenticationToken.java
new file mode 100755
index 0000000000..cd21dd29e6
--- /dev/null
+++ b/ntlm/src/main/java/org/acegisecurity/ui/ntlm/NtlmUsernamePasswordAuthenticationToken.java
@@ -0,0 +1,49 @@
+/* Copyright 2004-2007 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.acegisecurity.ui.ntlm;
+
+import jcifs.smb.NtlmPasswordAuthentication;
+
+import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
+
+/**
+ * An NTLM-specific {@link UsernamePasswordAuthenticationToken} that allows
+ * any provider to bypass the problem of an empty password since NTLM does
+ * not retrieve the user's password from the PDC.
+ *
+ * @author Sylvain Mougenot
+ */
+public class NtlmUsernamePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * ACEGI often checks password ; but we do not have one. This is the replacement password
+ */
+ public static final String DEFAULT_PASSWORD = "";
+
+ /**
+ * Create an NTLM {@link UsernamePasswordAuthenticationToken} using the
+ * JCIFS {@link NtlmPasswordAuthentication} object.
+ *
+ * @param ntlmAuth The {@link NtlmPasswordAuthentication} object.
+ * @param stripDomain Uses just the username if true
,
+ * otherwise use the username and domain name.
+ */
+ public NtlmUsernamePasswordAuthenticationToken(final NtlmPasswordAuthentication ntlmAuth, final boolean stripDomain) {
+ super((stripDomain) ? ntlmAuth.getUsername() : ntlmAuth.getName(), DEFAULT_PASSWORD);
+ }
+}
diff --git a/ntlm/src/main/java/org/acegisecurity/ui/ntlm/ldap/authenticator/NtlmAwareLdapAuthenticationProvider.java b/ntlm/src/main/java/org/acegisecurity/ui/ntlm/ldap/authenticator/NtlmAwareLdapAuthenticationProvider.java
new file mode 100755
index 0000000000..358f49fd83
--- /dev/null
+++ b/ntlm/src/main/java/org/acegisecurity/ui/ntlm/ldap/authenticator/NtlmAwareLdapAuthenticationProvider.java
@@ -0,0 +1,108 @@
+/**
+ *
+ */
+package org.acegisecurity.ui.ntlm.ldap.authenticator;
+
+import org.acegisecurity.*;
+import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
+import org.acegisecurity.providers.ldap.LdapAuthenticationProvider;
+import org.acegisecurity.providers.ldap.LdapAuthoritiesPopulator;
+import org.acegisecurity.ui.ntlm.NtlmUsernamePasswordAuthenticationToken;
+import org.acegisecurity.userdetails.UserDetails;
+import org.acegisecurity.userdetails.ldap.LdapUserDetails;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.dao.DataAccessException;
+import org.springframework.util.StringUtils;
+import org.springframework.ldap.core.DirContextOperations;
+
+/**
+ * This provider implements specialized behaviour if the supplied {@link Authentication} object is
+ * from NTLM. In other cases calls the parent implementation.
+ *
+ * @author sylvain.mougenot
+ *
+ */
+public class NtlmAwareLdapAuthenticationProvider extends LdapAuthenticationProvider {
+ private static final Log logger = LogFactory.getLog(NtlmAwareLdapAuthenticationProvider.class);
+
+ /**
+ * NTLM aware authenticator
+ */
+ private NtlmAwareLdapAuthenticator authenticator;
+
+ /**
+ * @param authenticator
+ * @param authoritiesPopulator
+ */
+ public NtlmAwareLdapAuthenticationProvider(NtlmAwareLdapAuthenticator authenticator,
+ LdapAuthoritiesPopulator authoritiesPopulator) {
+ super(authenticator, authoritiesPopulator);
+ this.authenticator = authenticator;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.acegisecurity.providers.ldap.LdapAuthenticationProvider#retrieveUser(java.lang.String,
+ * org.acegisecurity.providers.UsernamePasswordAuthenticationToken)
+ */
+ protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
+ throws AuthenticationException {
+ final UserDetails myDetails;
+
+ if (authentication instanceof NtlmUsernamePasswordAuthenticationToken) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Ntlm Token for Authentication"); //$NON-NLS-1$
+ }
+
+ // Only loads LDAP data
+ myDetails = retrieveUser(username, (NtlmUsernamePasswordAuthenticationToken) authentication);
+ } else {
+ // calls parent implementation
+ myDetails = super.retrieveUser(username, authentication);
+ }
+
+ return myDetails;
+ }
+
+ /**
+ * Authentication has already been done. We need a particular behviour
+ * because the parent check password consistency. But we do not have the
+ * password (even if the user is authenticated).
+ *
+ * @see NtlmUsernamePasswordAuthenticationToken#DEFAULT_PASSWORD
+ * @param username
+ * @param authentication
+ * @return
+ * @throws AuthenticationException
+ */
+ protected UserDetails retrieveUser(String username, NtlmUsernamePasswordAuthenticationToken authentication)
+ throws AuthenticationException {
+ // identifiant obligatoire
+ if (!StringUtils.hasLength(username)) {
+ throw new BadCredentialsException(messages.getMessage(
+ "LdapAuthenticationProvider.emptyUsername",
+ "Empty Username"));
+ }
+
+ // NB: password is just the default value
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("Retrieving user " + username);
+ }
+
+ try {
+ // Complies with our lack of password (can't bind)
+ DirContextOperations ldapUser = authenticator.authenticate(authentication);
+
+ GrantedAuthority[] extraAuthorities = getAuthoritiesPopulator().getGrantedAuthorities(ldapUser, username);
+
+ return getUserDetailsContextMapper().mapUserFromContext(ldapUser, username, extraAuthorities);
+
+ } catch (DataAccessException ldapAccessFailure) {
+ throw new AuthenticationServiceException(ldapAccessFailure
+ .getMessage(), ldapAccessFailure);
+ }
+ }
+}
diff --git a/ntlm/src/main/java/org/acegisecurity/ui/ntlm/ldap/authenticator/NtlmAwareLdapAuthenticator.java b/ntlm/src/main/java/org/acegisecurity/ui/ntlm/ldap/authenticator/NtlmAwareLdapAuthenticator.java
new file mode 100755
index 0000000000..15a76cb5c4
--- /dev/null
+++ b/ntlm/src/main/java/org/acegisecurity/ui/ntlm/ldap/authenticator/NtlmAwareLdapAuthenticator.java
@@ -0,0 +1,25 @@
+/**
+ *
+ */
+package org.acegisecurity.ui.ntlm.ldap.authenticator;
+
+import org.acegisecurity.providers.ldap.LdapAuthenticator;
+import org.acegisecurity.ui.ntlm.NtlmUsernamePasswordAuthenticationToken;
+import org.springframework.ldap.core.DirContextOperations;
+
+/**
+ * Authenticator compliant with NTLM part done previously (for authentication).
+ *
+ * @author sylvain.mougenot
+ *
+ */
+public interface NtlmAwareLdapAuthenticator extends LdapAuthenticator {
+ /**
+ * Authentication was done previously by NTLM.
+ * Obtains additional user informations from the directory.
+ *
+ * @param aUserToken Ntlm issued authentication Token
+ * @return the details of the successfully authenticated user.
+ */
+ DirContextOperations authenticate(NtlmUsernamePasswordAuthenticationToken aUserToken);
+}
diff --git a/ntlm/src/main/java/org/acegisecurity/ui/ntlm/ldap/authenticator/NtlmAwareLdapAuthenticatorImpl.java b/ntlm/src/main/java/org/acegisecurity/ui/ntlm/ldap/authenticator/NtlmAwareLdapAuthenticatorImpl.java
new file mode 100755
index 0000000000..78fde50eef
--- /dev/null
+++ b/ntlm/src/main/java/org/acegisecurity/ui/ntlm/ldap/authenticator/NtlmAwareLdapAuthenticatorImpl.java
@@ -0,0 +1,121 @@
+/**
+ *
+ */
+package org.acegisecurity.ui.ntlm.ldap.authenticator;
+
+import java.util.Iterator;
+
+import org.acegisecurity.BadCredentialsException;
+import org.acegisecurity.Authentication;
+import org.acegisecurity.ldap.InitialDirContextFactory;
+import org.acegisecurity.ldap.SpringSecurityLdapTemplate;
+import org.acegisecurity.providers.ldap.authenticator.BindAuthenticator;
+import org.acegisecurity.ui.ntlm.NtlmUsernamePasswordAuthenticationToken;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.ldap.core.DirContextOperations;
+
+/**
+ * Fullfill the User details after NTLM authentication was done. Or (if no NTLM
+ * authentication done) act as the parent to authenticate the user
+ *
+ * @author sylvain.mougenot
+ *
+ */
+public class NtlmAwareLdapAuthenticatorImpl extends BindAuthenticator {
+ /**
+ * Logger for this class
+ */
+ private static final Log logger = LogFactory.getLog(NtlmAwareLdapAuthenticatorImpl.class);
+
+ /**
+ * @param initialDirContextFactory
+ */
+ public NtlmAwareLdapAuthenticatorImpl(InitialDirContextFactory initialDirContextFactory) {
+ super(initialDirContextFactory);
+ }
+
+ /**
+ * Prepare the template without bind requirements.
+ *
+ * @param aUserDn
+ * @param aUserName
+ * @see #loadDetail(SpringSecurityLdapTemplate, String, String)
+ * @return
+ */
+ protected DirContextOperations bindWithoutDn(String aUserDn, String aUserName) {
+ SpringSecurityLdapTemplate template = new SpringSecurityLdapTemplate(getInitialDirContextFactory());
+ return loadDetail(template, aUserDn, aUserName);
+ }
+
+ /**
+ * Load datas
+ *
+ * @param aTemplate
+ * @param aUserDn
+ * @param aUserName
+ * @return
+ */
+ protected DirContextOperations loadDetail(SpringSecurityLdapTemplate aTemplate, String aUserDn, String aUserName) {
+ try {
+ DirContextOperations user = aTemplate.retrieveEntry(aUserDn, getUserAttributes());
+
+ return user;
+ } catch (BadCredentialsException e) {
+ // This will be thrown if an invalid user name is used and the
+ // method may
+ // be called multiple times to try different names, so we trap the
+ // exception
+ // unless a subclass wishes to implement more specialized behaviour.
+ if (logger.isDebugEnabled()) {
+ logger.debug("Failed to bind as " + aUserDn + ": "
+ + e.getMessage(), e);
+ }
+ }
+ return null;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.acegisecurity.ui.ntlm.NtlmAwareLdapAuthenticator#authenticate(org.acegisecurity.ui.ntlm.NtlmUsernamePasswordAuthenticationToken)
+ */
+ public DirContextOperations authenticate(Authentication authentication) {
+ if (!(authentication instanceof NtlmUsernamePasswordAuthenticationToken)) {
+ return super.authenticate(authentication);
+ }
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("authenticate(NtlmUsernamePasswordAuthenticationToken) - start"); //$NON-NLS-1$
+ }
+
+ final String userName = authentication.getName();
+ DirContextOperations user = null;
+
+ // If DN patterns are configured, try authenticating with them directly
+ Iterator myDns = getUserDns(userName).iterator();
+
+ // tries them all until we found something
+ while (myDns.hasNext() && (user == null)) {
+ user = bindWithoutDn((String) myDns.next(), userName);
+ }
+
+ // Otherwise use the configured locator to find the user
+ // and authenticate with the returned DN.
+ if ((user == null) && (getUserSearch() != null)) {
+ DirContextOperations userFromSearch = getUserSearch().searchForUser(userName);
+ // lancer l'identificvation
+ user = bindWithoutDn(userFromSearch.getDn().toString(), userName);
+ }
+
+ // Failed to locate the user in the LDAP directory
+ if (user == null) {
+ throw new BadCredentialsException(messages.getMessage("BindAuthenticator.badCredentials", "Bad credentials"));
+ }
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("authenticate(NtlmUsernamePasswordAuthenticationToken) - end"); //$NON-NLS-1$
+ }
+ return user;
+ }
+}
diff --git a/ntlm/web.xml b/ntlm/web.xml
new file mode 100755
index 0000000000..096ddf75b5
--- /dev/null
+++ b/ntlm/web.xml
@@ -0,0 +1,57 @@
+
+