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 @@ + + + + + + + + + CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON + PATTERN_TYPE_APACHE_ANT + /login_error.jsp=httpSessionContextIntegrationFilter + /**=httpSessionContextIntegrationFilter, exceptionTranslationFilter, ntlmFilter, filterSecurityInterceptor + + + + + + + + org.acegisecurity.context.SecurityContextImpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + jdoe=PASSWORD,ROLE_USER + + + + + + + + + + CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON + PATTERN_TYPE_APACHE_ANT + /**=ROLE_USER + + + + + + + + false + + + + + + + + + + + + + + + + + + + + diff --git a/ntlm/pom.xml b/ntlm/pom.xml new file mode 100755 index 0000000000..47fac464f9 --- /dev/null +++ b/ntlm/pom.xml @@ -0,0 +1,65 @@ + + 4.0.0 + + org.acegisecurity + acegi-security-parent + 2.0-SNAPSHOT + + jar + spring-security-ntlm + Spring Security - NTLM + + + + org.acegisecurity + acegi-security + ${project.version} + + + + org.samba.jcifs + jcifs + 1.2.15 + + + javax.servlet + jsp-api + 2.0 + true + + + javax.servlet + servlet-api + 2.4 + true + + + org.springframework.ldap + spring-ldap + 1.2-RC1 + true + + + + + + + ${basedir}/../ + META-INF + + notice.txt + + false + + + ${basedir}/src/main/resources + / + + **/* + + false + + + + + \ No newline at end of file diff --git a/ntlm/src/main/java/org/acegisecurity/ui/ntlm/HttpFilter.java b/ntlm/src/main/java/org/acegisecurity/ui/ntlm/HttpFilter.java new file mode 100755 index 0000000000..6c257dbc89 --- /dev/null +++ b/ntlm/src/main/java/org/acegisecurity/ui/ntlm/HttpFilter.java @@ -0,0 +1,61 @@ +/* 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 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; + +public abstract class HttpFilter implements Filter { + + public void init(FilterConfig config) throws ServletException {} + + public void destroy() {} + + public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException { + if (!(request instanceof HttpServletRequest)) { + throw new ServletException("Can only process HttpServletRequest"); + } + + if (!(response instanceof HttpServletResponse)) { + throw new ServletException("Can only process HttpServletResponse"); + } + + this.doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain); + + chain.doFilter(request, response); + } + +// *************************** Protected Methods **************************** + + protected abstract void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException; + + protected void sendRedirect(final HttpServletRequest request, final HttpServletResponse response, String url) throws IOException { + if (!url.startsWith("http://") && !url.startsWith("https://")) { + url = request.getContextPath() + url; + } + + response.sendRedirect(response.encodeRedirectURL(url)); + } + +} // End HttpFilter diff --git a/ntlm/src/main/java/org/acegisecurity/ui/ntlm/NtlmBaseException.java b/ntlm/src/main/java/org/acegisecurity/ui/ntlm/NtlmBaseException.java new file mode 100755 index 0000000000..e9948fd070 --- /dev/null +++ b/ntlm/src/main/java/org/acegisecurity/ui/ntlm/NtlmBaseException.java @@ -0,0 +1,34 @@ +/* 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; + +/** + * Base class for NTLM exceptions so that it is easier to distinguish them + * from other AuthenticationExceptions 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}: + *

+ *

+ * + * If the {@link AuthenticationException} is not a subclass of + * {@link NtlmBaseException}, then redirect the user to the authentication + * failure URL. + * + * @param request The {@link HttpServletRequest} object. + * @param response Then {@link HttpServletResponse} object. + * @param authException Either {@link NtlmBeginHandshakeException}, + * {@link NtlmType2MessageException}, or + * {@link AuthenticationException} + */ + public void commence(final ServletRequest request, final ServletResponse response, final AuthenticationException authException) throws IOException, ServletException { + final HttpServletResponse resp = (HttpServletResponse) response; + + if (authException instanceof NtlmBaseException) { + if (authException instanceof NtlmType2MessageException) { + ((NtlmType2MessageException) authException).preserveAuthentication(); + } + resp.setHeader("WWW-Authenticate", authException.getMessage()); + resp.setHeader("Connection", "Keep-Alive"); + resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + resp.setContentLength(0); + resp.flushBuffer(); + } else { + String url = authenticationFailureUrl; + if (!url.startsWith("http://") && !url.startsWith("https://")) { + url = ((HttpServletRequest) request).getContextPath() + url; + } + + resp.sendRedirect(resp.encodeRedirectURL(url)); + } + } + +} // End NtlmProcessingFilterEntryPoint diff --git a/ntlm/src/main/java/org/acegisecurity/ui/ntlm/NtlmType2MessageException.java b/ntlm/src/main/java/org/acegisecurity/ui/ntlm/NtlmType2MessageException.java new file mode 100755 index 0000000000..c1e9702240 --- /dev/null +++ b/ntlm/src/main/java/org/acegisecurity/ui/ntlm/NtlmType2MessageException.java @@ -0,0 +1,48 @@ +/* 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.Authentication; +import org.acegisecurity.AuthenticationException; +import org.acegisecurity.context.SecurityContextHolder; + +/** + * Contains the NTLM Type 2 message that is sent back to the client during + * negotiations. + * + * @author Edward Smith + */ +public class NtlmType2MessageException extends NtlmBaseException { + + private static final long serialVersionUID = 1L; + + private final Authentication auth; + + public NtlmType2MessageException(final String type2Msg) { + super("NTLM " + type2Msg); + auth = SecurityContextHolder.getContext().getAuthentication(); + } + + /** + * Preserve the existing 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 @@ + + + Acegi NTLM + + + + + + contextConfigLocation + /WEB-INF/applicationContext.xml + + + + log4jConfigLocation + /WEB-INF/log4j.properties + + + + + Acegi Filter Chain Proxy + org.acegisecurity.util.FilterToBeanProxy + + targetClass + org.acegisecurity.util.FilterChainProxy + + + + + Acegi Filter Chain Proxy + /** + + + + + + org.springframework.web.context.ContextLoaderListener + + + + + org.springframework.web.util.Log4jConfigListener + + + + + org.acegisecurity.ui.session.HttpSessionEventPublisher + + + + index.html + index.htm + index.jsp + default.html + default.htm + default.jsp + +