From 827d0e1ebfd277ab5e696d986faf294e69348666 Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Fri, 15 Aug 2008 22:44:22 +0000 Subject: [PATCH] OPEN - issue SEC-865: Re-Challenge NTLM Clients after Authentication Failure http://jira.springframework.org/browse/SEC-865. Changed NTLM filter to re-challenge if retryOnAuthFailure is set and the Smb logon call fails. Updated JCIFS version in pom. --- ntlm/pom.xml | 88 +- .../ui/ntlm/NtlmProcessingFilter.java | 797 +++++++++--------- 2 files changed, 446 insertions(+), 439 deletions(-) diff --git a/ntlm/pom.xml b/ntlm/pom.xml index 47e5847518..9161de7536 100755 --- a/ntlm/pom.xml +++ b/ntlm/pom.xml @@ -1,36 +1,36 @@ - 4.0.0 - - org.springframework.security - spring-security-parent - 2.0.4-SNAPSHOT - - jar - spring-security-ntlm - Spring Security - NTLM support + 4.0.0 + + org.springframework.security + spring-security-parent + 2.0.4-SNAPSHOT + + jar + spring-security-ntlm + Spring Security - NTLM support - - - org.springframework.security - spring-security-core - ${project.version} - - - - org.samba.jcifs - jcifs - 1.2.15 - - - javax.servlet - jsp-api - 2.0 - true - - - javax.servlet - servlet-api - + + + org.springframework.security + spring-security-core + ${project.version} + + + + org.samba.jcifs + jcifs + 1.2.19 + + + javax.servlet + jsp-api + 2.0 + true + + + javax.servlet + servlet-api + org.springframework.ldap spring-ldap @@ -38,17 +38,17 @@ - - - - ${basedir}/src/main/resources - / - - **/* - - false - - - + + + + ${basedir}/src/main/resources + / + + **/* + + false + + + - \ No newline at end of file + diff --git a/ntlm/src/main/java/org/springframework/security/ui/ntlm/NtlmProcessingFilter.java b/ntlm/src/main/java/org/springframework/security/ui/ntlm/NtlmProcessingFilter.java index 615d5cde02..230459ee71 100755 --- a/ntlm/src/main/java/org/springframework/security/ui/ntlm/NtlmProcessingFilter.java +++ b/ntlm/src/main/java/org/springframework/security/ui/ntlm/NtlmProcessingFilter.java @@ -82,221 +82,222 @@ import java.util.Properties; * @version $Id$ */ public class NtlmProcessingFilter extends SpringSecurityFilter implements InitializingBean { - //~ Static fields/initializers ===================================================================================== + //~ Static fields/initializers ===================================================================================== - private static Log logger = LogFactory.getLog(NtlmProcessingFilter.class); + private static Log logger = LogFactory.getLog(NtlmProcessingFilter.class); - private static final String STATE_ATTR = "SpringSecurityNtlm"; - private static final String CHALLENGE_ATTR = "NtlmChal"; - private static final Integer BEGIN = new Integer(0); - private static final Integer NEGOTIATE = new Integer(1); - private static final Integer COMPLETE = new Integer(2); - private static final Integer DELAYED = new Integer(3); + private static final String STATE_ATTR = "SpringSecurityNtlm"; + private static final String CHALLENGE_ATTR = "NtlmChal"; + private static final Integer BEGIN = new Integer(0); + private static final Integer NEGOTIATE = new Integer(1); + private static final Integer COMPLETE = new Integer(2); + private static final Integer DELAYED = new Integer(3); //~ Instance fields ================================================================================================ - /** Should the filter load balance among multiple domain controllers, default false */ - private boolean loadBalance; + /** Should the filter load balance among multiple domain controllers, default false */ + private boolean loadBalance; - /** Should the domain name be stripped from the username, default true */ - private boolean stripDomain = true; + /** Should 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; + /** Should the filter initiate NTLM negotiations, default true */ + private boolean forceIdentification = true; - /** Should the filter retry NTLM on authorization failure, default false */ - private boolean retryOnAuthFailure; + /** Should 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; + private String soTimeout; + private String cachePolicy; + private String defaultDomain; + private String domainController; + private AuthenticationManager authenticationManager; private AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource(); //~ 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)); - } + /** + * 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; - } + } + + /** + * 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 occur if the credentials are rejected by the domain controller or if an + * an {@link AuthenticationCredentialsNotFoundException} + * or {@link InsufficientAuthenticationException} is thrown. + * + * @param retryOnFailure the retry on failure flag value to set. + */ + public void setRetryOnAuthFailure(boolean retryOnFailure) { + this.retryOnAuthFailure = retryOnFailure; + } public void setAuthenticationDetailsSource(AuthenticationDetailsSource authenticationDetailsSource) { Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null"); @@ -305,207 +306,213 @@ public class NtlmProcessingFilter extends SpringSecurityFilter implements Initia protected void doFilterHttp(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) throws IOException, ServletException { - 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 Spring Security 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); - } - } - } + 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 Spring Security 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); + } + } + } chain.doFilter(request, response); } - /** - * 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 Spring - * 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(authenticationDetailsSource.buildDetails(request)); - - // Place the last username attempted into HttpSession for views - session.setAttribute(AuthenticationProcessingFilter.SPRING_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; + /** + * 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); + + if (retryOnAuthFailure) { + logger.debug("Restarting NTLM authentication handshake"); + session.setAttribute(STATE_ATTR, BEGIN); + throw new NtlmBeginHandshakeException(); + } + + throw new BadCredentialsException("Bad NTLM credentials"); + } finally { + session.removeAttribute(CHALLENGE_ATTR); + } + } + + /** + * Authenticates the user credentials acquired from NTLM against the Spring + * 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(authenticationDetailsSource.buildDetails(request)); + + // Place the last username attempted into HttpSession for views + session.setAttribute(AuthenticationProcessingFilter.SPRING_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); - } + } public int getOrder() { return FilterChainOrder.NTLM_FILTER;