1
2
3
4
5
6
7
8
9
10
11
12
13
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84 public class NtlmProcessingFilter extends SpringSecurityFilter implements InitializingBean {
85
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
97
98
99 private boolean loadBalance;
100
101
102 private boolean stripDomain = true;
103
104
105 private boolean forceIdentification = true;
106
107
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
118
119
120
121
122
123 public void afterPropertiesSet() throws Exception {
124 Assert.notNull(this.authenticationManager, "An AuthenticationManager is required");
125
126
127 Config.setProperty("jcifs.smb.client.soTimeout", soTimeout == null ? "300000" : soTimeout);
128
129 Config.setProperty("jcifs.netbios.cachePolicy", cachePolicy == null ? "1200" : cachePolicy);
130
131 if (domainController == null) {
132 domainController = defaultDomain;
133 }
134 }
135
136
137
138
139
140
141 public void setAuthenticationManager(AuthenticationManager authenticationManager) {
142 this.authenticationManager = authenticationManager;
143 }
144
145
146
147
148
149
150
151
152
153 public void setDefaultDomain(String defaultDomain) {
154 this.defaultDomain = defaultDomain;
155 Config.setProperty("jcifs.smb.client.domain", defaultDomain);
156 }
157
158
159
160
161
162
163 public void setSmbClientUsername(String smbClientUsername) {
164 Config.setProperty("jcifs.smb.client.username", smbClientUsername);
165 }
166
167
168
169
170
171
172 public void setSmbClientPassword(String smbClientPassword) {
173 Config.setProperty("jcifs.smb.client.password", smbClientPassword);
174 }
175
176
177
178
179
180
181
182
183
184
185 public void setSmbClientSSNLimit(String smbClientSSNLimit) {
186 Config.setProperty("jcifs.smb.client.ssnLimit", smbClientSSNLimit);
187 }
188
189
190
191
192
193
194
195
196 public void setNetbiosWINS(String netbiosWINS) {
197 Config.setProperty("jcifs.netbios.wins", netbiosWINS);
198 }
199
200
201
202
203
204
205
206 public void setDomainController(String domainController) {
207 this.domainController = domainController;
208 }
209
210
211
212
213
214
215
216
217
218 public void setLoadBalance(boolean loadBalance) {
219 this.loadBalance = loadBalance;
220 }
221
222
223
224
225
226
227
228
229 public void setStripDomain(boolean stripDomain) {
230 this.stripDomain = stripDomain;
231 }
232
233
234
235
236
237
238
239
240 public void setSoTimeout(String timeout) {
241 this.soTimeout = timeout;
242 }
243
244
245
246
247
248
249
250
251 public void setCachePolicy(String numSeconds) {
252 this.cachePolicy = numSeconds;
253 }
254
255
256
257
258
259
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
274
275
276
277 public boolean isForceIdentification() {
278 return this.forceIdentification;
279 }
280
281
282
283
284
285
286 public void setForceIdentification(boolean forceIdentification) {
287 this.forceIdentification = forceIdentification;
288 }
289
290
291
292
293
294
295
296
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
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
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
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
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
364
365
366
367
368
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
380
381
382
383
384
385
386
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
402
403
404
405
406
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
431
432
433
434
435
436
437
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
448 session.setAttribute(AuthenticationProcessingFilter.SPRING_SECURITY_LAST_USERNAME_KEY, authRequest.getName());
449
450
451 backupAuth = SecurityContextHolder.getContext().getAuthentication();
452
453 try {
454
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
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
474 SecurityContextHolder.getContext().setAuthentication(authResult);
475 }
476
477
478
479
480
481
482
483
484
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
501
502
503
504
505
506
507
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 }