View Javadoc

1   /* Copyright 2004-2007 Acegi Technology Pty Limited
2    *
3    * Licensed under the Apache License, Version 2.0 (the "License");
4    * you may not use this file except in compliance with the License.
5    * You may obtain a copy of the License at
6    *
7    *     http://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  
16  package org.springframework.security.ui.ntlm;
17  
18  import org.springframework.security.Authentication;
19  import org.springframework.security.AuthenticationCredentialsNotFoundException;
20  import org.springframework.security.AuthenticationException;
21  import org.springframework.security.AuthenticationManager;
22  import org.springframework.security.BadCredentialsException;
23  import org.springframework.security.InsufficientAuthenticationException;
24  import org.springframework.security.context.SecurityContextHolder;
25  import org.springframework.security.providers.UsernamePasswordAuthenticationToken;
26  import org.springframework.security.providers.anonymous.AnonymousAuthenticationToken;
27  import org.springframework.security.ui.SpringSecurityFilter;
28  import org.springframework.security.ui.FilterChainOrder;
29  import org.springframework.security.ui.AuthenticationDetailsSource;
30  import org.springframework.security.ui.WebAuthenticationDetailsSource;
31  import org.springframework.security.ui.webapp.AuthenticationProcessingFilter;
32  import org.springframework.beans.factory.InitializingBean;
33  import org.springframework.util.Assert;
34  
35  import jcifs.Config;
36  import jcifs.UniAddress;
37  import jcifs.ntlmssp.Type1Message;
38  import jcifs.ntlmssp.Type2Message;
39  import jcifs.ntlmssp.Type3Message;
40  import jcifs.smb.NtlmChallenge;
41  import jcifs.smb.NtlmPasswordAuthentication;
42  import jcifs.smb.SmbAuthException;
43  import jcifs.smb.SmbException;
44  import jcifs.smb.SmbSession;
45  import jcifs.util.Base64;
46  import org.apache.commons.logging.Log;
47  import org.apache.commons.logging.LogFactory;
48  
49  import javax.servlet.FilterChain;
50  import javax.servlet.ServletException;
51  import javax.servlet.http.HttpServletRequest;
52  import javax.servlet.http.HttpServletResponse;
53  import javax.servlet.http.HttpSession;
54  import java.io.IOException;
55  import java.net.UnknownHostException;
56  import java.util.Enumeration;
57  import java.util.Properties;
58  
59  /**
60   * A clean-room implementation for Spring Security of an NTLM HTTP filter
61   * leveraging the JCIFS library.
62   * <p>
63   * NTLM is a Microsoft-developed protocol providing single sign-on capabilities
64   * to web applications and other integrated applications.  It allows a web
65   * server to automatically discover the username of a browser client when that
66   * client is logged into a Windows domain and is using an NTLM-aware browser.
67   * A web application can then reuse the user's Windows credentials without
68   * having to ask for them again.
69   * <p>
70   * Because NTLM only provides the username of the Windows client, a Spring
71   * Security NTLM deployment must have a <code>UserDetailsService</code> that
72   * provides a <code>UserDetails</code> object with the empty string as the
73   * password and whatever <code>GrantedAuthority</code> values necessary to
74   * pass the <code>FilterSecurityInterceptor</code>.
75   * <p>
76   * The Spring Security bean configuration file must also place the
77   * <code>ExceptionTranslationFilter</code> before this filter in the
78   * <code>FilterChainProxy</code> definition.
79   *
80   * @author Davide Baroncelli
81   * @author Edward Smith
82   * @version $Id: NtlmProcessingFilter.java 3249 2008-08-15 22:44:22Z luke_t $
83   */
84  public class NtlmProcessingFilter extends SpringSecurityFilter implements InitializingBean {
85      //~ Static fields/initializers =====================================================================================
86  
87      private static Log    logger = LogFactory.getLog(NtlmProcessingFilter.class);
88  
89      private static final String    STATE_ATTR = "SpringSecurityNtlm";
90      private static final String    CHALLENGE_ATTR = "NtlmChal";
91      private static final Integer BEGIN = new Integer(0);
92      private static final Integer NEGOTIATE = new Integer(1);
93      private static final Integer COMPLETE = new Integer(2);
94      private static final Integer DELAYED = new Integer(3);
95  
96      //~ Instance fields ================================================================================================
97  
98      /** Should the filter load balance among multiple domain controllers, default <code>false</code> */
99      private boolean    loadBalance;
100 
101     /** Should the domain name be stripped from the username, default <code>true</code> */
102     private boolean stripDomain = true;
103 
104     /** Should the filter initiate NTLM negotiations, default <code>true</code>    */
105     private boolean forceIdentification = true;
106 
107     /** Should the filter retry NTLM on authorization failure, default <code>false</code> */
108     private boolean retryOnAuthFailure;
109 
110     private String    soTimeout;
111     private String    cachePolicy;
112     private String    defaultDomain;
113     private String    domainController;
114     private AuthenticationManager authenticationManager;
115     private AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource();
116 
117     //~ Methods ========================================================================================================
118 
119     /**
120      * Ensures an <code>AuthenticationManager</code> and authentication failure
121      * URL have been provided in the bean configuration file.
122      */
123     public void afterPropertiesSet() throws Exception {
124         Assert.notNull(this.authenticationManager, "An AuthenticationManager is required");
125 
126         // Default to 5 minutes if not already specified
127         Config.setProperty("jcifs.smb.client.soTimeout", soTimeout == null ? "300000" : soTimeout);
128         // Default to 20 minutes if not already specified
129         Config.setProperty("jcifs.netbios.cachePolicy", cachePolicy == null ? "1200" : cachePolicy);
130 
131         if (domainController == null) {
132             domainController = defaultDomain;
133         }
134     }
135 
136     /**
137      * Sets the <code>AuthenticationManager</code> to use.
138      *
139      * @param authenticationManager the <code>AuthenticationManager</code> to use.
140      */
141     public void setAuthenticationManager(AuthenticationManager authenticationManager) {
142         this.authenticationManager = authenticationManager;
143     }
144 
145     /**
146      * The NT domain against which clients should be authenticated. If the SMB
147      * client username and password are also set, then preauthentication will
148      * be used which is necessary to initialize the SMB signing digest. SMB
149      * signatures are required by default on Windows 2003 domain controllers.
150      *
151      * @param defaultDomain The name of the default domain.
152      */
153     public void setDefaultDomain(String defaultDomain) {
154         this.defaultDomain = defaultDomain;
155         Config.setProperty("jcifs.smb.client.domain", defaultDomain);
156     }
157 
158     /**
159      * Sets the SMB client username.
160      *
161      * @param smbClientUsername The SMB client username.
162      */
163     public void setSmbClientUsername(String smbClientUsername) {
164         Config.setProperty("jcifs.smb.client.username", smbClientUsername);
165     }
166 
167     /**
168      * Sets the SMB client password.
169      *
170      * @param smbClientPassword The SMB client password.
171      */
172     public void setSmbClientPassword(String smbClientPassword) {
173         Config.setProperty("jcifs.smb.client.password", smbClientPassword);
174     }
175 
176     /**
177      * Sets the SMB client SSN limit. When set to <code>1</code>, every
178      * authentication is forced to use a separate transport. This effectively
179      * ignores SMB signing requirements, however at the expense of reducing
180      * scalability. Preauthentication with a domain, username, and password is
181      * the preferred method for working with servers that require signatures.
182      *
183      * @param smbClientSSNLimit The SMB client SSN limit.
184      */
185     public void setSmbClientSSNLimit(String smbClientSSNLimit) {
186         Config.setProperty("jcifs.smb.client.ssnLimit", smbClientSSNLimit);
187     }
188 
189     /**
190      * Configures JCIFS to use a WINS server.  It is preferred to use a WINS
191      * server over a specific domain controller.  Set this property instead of
192      * <code>domainController</code> if there is a WINS server available.
193      *
194      * @param netbiosWINS The WINS server JCIFS will use.
195      */
196     public void setNetbiosWINS(String netbiosWINS) {
197         Config.setProperty("jcifs.netbios.wins", netbiosWINS);
198     }
199 
200     /**
201      * The IP address of any SMB server that should be used to authenticate
202      * HTTP clients.
203      *
204      * @param domainController The IP address of the domain controller.
205      */
206     public void setDomainController(String domainController) {
207         this.domainController = domainController;
208     }
209 
210     /**
211      * If the default domain is specified and the domain controller is not
212      * specified, then query for domain controllers by name.  When load
213      * balance is <code>true</code>, rotate through the list of domain
214      * controllers when authenticating users.
215      *
216      * @param loadBalance The load balance flag value.
217      */
218     public void setLoadBalance(boolean loadBalance) {
219         this.loadBalance = loadBalance;
220     }
221 
222     /**
223      * Configures <code>NtlmProcessingFilter</code> to strip the Windows
224      * domain name from the username when set to <code>true</code>, which
225      * is the default value.
226      *
227      * @param stripDomain The strip domain flag value.
228      */
229     public void setStripDomain(boolean stripDomain) {
230         this.stripDomain = stripDomain;
231     }
232 
233     /**
234      * Sets the <code>jcifs.smb.client.soTimeout</code> property to the
235      * timeout value specified in milliseconds. Defaults to 5 minutes
236      * if not specified.
237      *
238      * @param timeout The milliseconds timeout value.
239      */
240     public void setSoTimeout(String timeout) {
241         this.soTimeout = timeout;
242     }
243 
244     /**
245      * Sets the <code>jcifs.netbios.cachePolicy</code> property to the
246      * number of seconds a NetBIOS address is cached by JCIFS. Defaults to
247      * 20 minutes if not specified.
248      *
249      * @param numSeconds The number of seconds a NetBIOS address is cached.
250      */
251     public void setCachePolicy(String numSeconds) {
252         this.cachePolicy = numSeconds;
253     }
254 
255     /**
256      * Loads properties starting with "jcifs" into the JCIFS configuration.
257      * Any other properties are ignored.
258      *
259      * @param props The JCIFS properties to set.
260      */
261     public void setJcifsProperties(Properties props) {
262         String name;
263 
264         for (Enumeration e=props.keys(); e.hasMoreElements();) {
265             name = (String) e.nextElement();
266             if (name.startsWith("jcifs.")) {
267                 Config.setProperty(name, props.getProperty(name));
268             }
269         }
270     }
271 
272     /**
273      * Returns <code>true</code> if NTLM authentication is forced.
274      *
275      * @return <code>true</code> if NTLM authentication is forced.
276      */
277     public boolean isForceIdentification() {
278         return this.forceIdentification;
279     }
280 
281     /**
282      * Sets a flag denoting whether NTLM authentication should be forced.
283      *
284      * @param forceIdentification the force identification flag value to set.
285      */
286     public void setForceIdentification(boolean forceIdentification) {
287         this.forceIdentification = forceIdentification;
288     }
289 
290     /**
291      * Sets a flag denoting whether NTLM should retry whenever authentication
292      * fails.  Retry will occur if the credentials are rejected by the domain controller or if an
293      * an {@link AuthenticationCredentialsNotFoundException}
294      * or {@link InsufficientAuthenticationException} is thrown.
295      *
296      * @param retryOnFailure the retry on failure flag value to set.
297      */
298     public void setRetryOnAuthFailure(boolean retryOnFailure) {
299         this.retryOnAuthFailure = retryOnFailure;
300     }
301 
302     public void setAuthenticationDetailsSource(AuthenticationDetailsSource authenticationDetailsSource) {
303         Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null");
304         this.authenticationDetailsSource = authenticationDetailsSource;
305     }
306 
307     protected void doFilterHttp(final HttpServletRequest request,
308             final HttpServletResponse response, final FilterChain chain) throws IOException, ServletException {
309         final HttpSession session = request.getSession();
310         Integer ntlmState = (Integer) session.getAttribute(STATE_ATTR);
311 
312         // Start NTLM negotiations the first time through the filter
313         if (ntlmState == null) {
314             if (forceIdentification) {
315                 logger.debug("Starting NTLM handshake");
316                 session.setAttribute(STATE_ATTR, BEGIN);
317                 throw new NtlmBeginHandshakeException();
318             } else {
319                 logger.debug("NTLM handshake not yet started");
320                 session.setAttribute(STATE_ATTR, DELAYED);
321             }
322         }
323 
324         // IE will send a Type 1 message to reauthenticate the user during an HTTP POST
325         if (ntlmState == COMPLETE && this.reAuthOnIEPost(request))
326             ntlmState = BEGIN;
327 
328         final String authMessage = request.getHeader("Authorization");
329         if (ntlmState != COMPLETE && authMessage != null && authMessage.startsWith("NTLM ")) {
330             final UniAddress dcAddress = this.getDCAddress(session);
331             if (ntlmState == BEGIN) {
332                 logger.debug("Processing NTLM Type 1 Message");
333                 session.setAttribute(STATE_ATTR, NEGOTIATE);
334                 this.processType1Message(authMessage, session, dcAddress);
335             } else {
336                 logger.debug("Processing NTLM Type 3 Message");
337                 final NtlmPasswordAuthentication auth = this.processType3Message(authMessage, session, dcAddress);
338                 logger.debug("NTLM negotiation complete");
339                 this.logon(session, dcAddress, auth);
340                 session.setAttribute(STATE_ATTR, COMPLETE);
341 
342                 // Do not reauthenticate the user in Spring Security during an IE POST
343                 final Authentication myCurrentAuth = SecurityContextHolder.getContext().getAuthentication();
344                 if (myCurrentAuth == null || myCurrentAuth instanceof AnonymousAuthenticationToken) {
345                     logger.debug("Authenticating user credentials");
346                     this.authenticate(request, response, session, auth);
347                 }
348             }
349         }
350 
351         chain.doFilter(request, response);
352     }
353 
354     /**
355      * Returns <code>true</code> if reauthentication is needed on an IE POST.
356      */
357     private boolean reAuthOnIEPost(final HttpServletRequest request) {
358         String ua = request.getHeader("User-Agent");
359         return (request.getMethod().equalsIgnoreCase("POST") && ua != null && ua.indexOf("MSIE") != -1);
360     }
361 
362     /**
363      * Creates and returns a Type 2 message from the provided Type 1 message.
364      *
365      * @param message the Type 1 message to process.
366      * @param session the <code>HTTPSession</code> object.
367      * @param dcAddress the domain controller address.
368      * @throws IOException
369      */
370     private void processType1Message(final String message, final HttpSession session, final UniAddress dcAddress) throws IOException {
371         final Type2Message type2msg = new Type2Message(
372                 new Type1Message(Base64.decode(message.substring(5))),
373                 this.getChallenge(session, dcAddress),
374                 null);
375         throw new NtlmType2MessageException(Base64.encode(type2msg.toByteArray()));
376     }
377 
378     /**
379      * Builds and returns an <code>NtlmPasswordAuthentication</code> object
380      * from the provided Type 3 message.
381      *
382      * @param message the Type 3 message to process.
383      * @param session the <code>HTTPSession</code> object.
384      * @param dcAddress the domain controller address.
385      * @return an <code>NtlmPasswordAuthentication</code> object.
386      * @throws IOException
387      */
388     private NtlmPasswordAuthentication processType3Message(final String message, final HttpSession session, final UniAddress dcAddress) throws IOException {
389         final Type3Message type3msg = new Type3Message(Base64.decode(message.substring(5)));
390         final byte[] lmResponse = (type3msg.getLMResponse() != null) ? type3msg.getLMResponse() : new byte[0];
391         final byte[] ntResponse = (type3msg.getNTResponse() != null) ? type3msg.getNTResponse() : new byte[0];
392         return new NtlmPasswordAuthentication(
393                 type3msg.getDomain(),
394                 type3msg.getUser(),
395                 this.getChallenge(session, dcAddress),
396                 lmResponse,
397                 ntResponse);
398     }
399 
400     /**
401      * Checks the user credentials against the domain controller.
402      *
403      * @param session the <code>HTTPSession</code> object.
404      * @param dcAddress the domain controller address.
405      * @param auth the <code>NtlmPasswordAuthentication</code> object.
406      * @throws IOException
407      */
408     private void logon(final HttpSession session, final UniAddress dcAddress, final NtlmPasswordAuthentication auth) throws IOException {
409         try {
410             SmbSession.logon(dcAddress, auth);
411             if (logger.isDebugEnabled()) {
412                 logger.debug(auth + " successfully authenticated against " + dcAddress);
413             }
414         } catch(SmbAuthException e) {
415             logger.error("Credentials " + auth + " were not accepted by the domain controller " + dcAddress);
416 
417             if (retryOnAuthFailure) {
418                 logger.debug("Restarting NTLM authentication handshake");
419                 session.setAttribute(STATE_ATTR, BEGIN);
420                 throw new NtlmBeginHandshakeException();
421             }
422 
423             throw new BadCredentialsException("Bad NTLM credentials");
424         } finally {
425             session.removeAttribute(CHALLENGE_ATTR);
426         }
427     }
428 
429     /**
430      * Authenticates the user credentials acquired from NTLM against the Spring
431      * Security <code>AuthenticationManager</code>.
432      *
433      * @param request the <code>HttpServletRequest</code> object.
434      * @param response the <code>HttpServletResponse</code> object.
435      * @param session the <code>HttpSession</code> object.
436      * @param auth the <code>NtlmPasswordAuthentication</code> object.
437      * @throws IOException
438      */
439     private void authenticate(final HttpServletRequest request, final HttpServletResponse response, final HttpSession session, final NtlmPasswordAuthentication auth) throws IOException {
440         final Authentication authResult;
441         final UsernamePasswordAuthenticationToken authRequest;
442         final Authentication backupAuth;
443 
444         authRequest = new NtlmUsernamePasswordAuthenticationToken(auth, stripDomain);
445         authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
446 
447         // Place the last username attempted into HttpSession for views
448         session.setAttribute(AuthenticationProcessingFilter.SPRING_SECURITY_LAST_USERNAME_KEY, authRequest.getName());
449 
450         // Backup the current authentication in case of an AuthenticationException
451         backupAuth = SecurityContextHolder.getContext().getAuthentication();
452 
453         try {
454             // Authenitcate the user with the authentication manager
455             authResult = authenticationManager.authenticate(authRequest);
456         } catch (AuthenticationException failed) {
457             if (logger.isInfoEnabled()) {
458                 logger.info("Authentication request for user: " + authRequest.getName() + " failed: " + failed.toString());
459             }
460 
461             // Reset the backup Authentication object and rethrow the AuthenticationException
462             SecurityContextHolder.getContext().setAuthentication(backupAuth);
463 
464             if (retryOnAuthFailure && (failed instanceof AuthenticationCredentialsNotFoundException || failed instanceof InsufficientAuthenticationException)) {
465                 logger.debug("Restart NTLM authentication handshake due to AuthenticationException");
466                 session.setAttribute(STATE_ATTR, BEGIN);
467                 throw new NtlmBeginHandshakeException();
468             }
469 
470             throw failed;
471         }
472 
473         // Set the Authentication object with the valid authentication result
474         SecurityContextHolder.getContext().setAuthentication(authResult);
475     }
476 
477     /**
478      * Returns the domain controller address based on the <code>loadBalance</code>
479      * setting.
480      *
481      * @param session the <code>HttpSession</code> object.
482      * @return the domain controller address.
483      * @throws UnknownHostException
484      * @throws SmbException
485      */
486     private UniAddress getDCAddress(final HttpSession session) throws UnknownHostException, SmbException {
487         if (loadBalance) {
488             NtlmChallenge chal = (NtlmChallenge) session.getAttribute(CHALLENGE_ATTR);
489             if (chal == null) {
490                 chal = SmbSession.getChallengeForDomain();
491                 session.setAttribute(CHALLENGE_ATTR, chal);
492             }
493             return chal.dc;
494         }
495 
496         return UniAddress.getByName(domainController, true);
497     }
498 
499     /**
500      * Returns the domain controller challenge based on the <code>loadBalance</code>
501      * setting.
502      *
503      * @param session the <code>HttpSession</code> object.
504      * @param dcAddress the domain controller address.
505      * @return the domain controller challenge.
506      * @throws UnknownHostException
507      * @throws SmbException
508      */
509     private byte[] getChallenge(final HttpSession session, final UniAddress dcAddress) throws UnknownHostException, SmbException {
510         if (loadBalance) {
511             return ((NtlmChallenge) session.getAttribute(CHALLENGE_ATTR)).challenge;
512         }
513 
514         return SmbSession.getChallenge(dcAddress);
515     }
516 
517     public int getOrder() {
518         return FilterChainOrder.NTLM_FILTER;
519     }
520 }