1
2
3
4
5
6
7
8
9
10
11
12
13 package org.eclipse.jetty.plus.jaas.spi;
14
15 import java.io.IOException;
16 import java.util.ArrayList;
17 import java.util.Hashtable;
18 import java.util.List;
19 import java.util.Map;
20 import java.util.Properties;
21
22 import javax.naming.Context;
23 import javax.naming.NamingEnumeration;
24 import javax.naming.NamingException;
25 import javax.naming.directory.Attribute;
26 import javax.naming.directory.Attributes;
27 import javax.naming.directory.DirContext;
28 import javax.naming.directory.InitialDirContext;
29 import javax.naming.directory.SearchControls;
30 import javax.naming.directory.SearchResult;
31 import javax.security.auth.Subject;
32 import javax.security.auth.callback.Callback;
33 import javax.security.auth.callback.CallbackHandler;
34 import javax.security.auth.callback.NameCallback;
35 import javax.security.auth.callback.UnsupportedCallbackException;
36 import javax.security.auth.login.LoginException;
37
38 import org.eclipse.jetty.http.security.Credential;
39 import org.eclipse.jetty.plus.jaas.callback.ObjectCallback;
40 import org.eclipse.jetty.util.log.Log;
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81 public class LdapLoginModule extends AbstractLoginModule
82 {
83
84
85
86 private String _hostname;
87
88
89
90
91 private int _port;
92
93
94
95
96 private String _authenticationMethod;
97
98
99
100
101 private String _contextFactory;
102
103
104
105
106 private String _bindDn;
107
108
109
110
111 private String _bindPassword;
112
113
114
115
116 private String _userObjectClass = "inetOrgPerson";
117
118
119
120
121 private String _userRdnAttribute = "uid";
122
123
124
125
126 private String _userIdAttribute = "cn";
127
128
129
130
131
132
133 private String _userPasswordAttribute = "userPassword";
134
135
136
137
138 private String _userBaseDn;
139
140
141
142
143 private String _roleBaseDn;
144
145
146
147
148 private String _roleObjectClass = "groupOfUniqueNames";
149
150
151
152
153 private String _roleMemberAttribute = "uniqueMember";
154
155
156
157
158 private String _roleNameAttribute = "roleName";
159
160 private boolean _debug;
161
162
163
164
165
166
167 private boolean _forceBindingLogin = false;
168
169
170
171
172 private boolean _useLdaps = false;
173
174 private DirContext _rootContext;
175
176
177
178
179
180
181
182
183
184
185
186
187
188 public UserInfo getUserInfo(String username) throws Exception
189 {
190 String pwdCredential = getUserCredentials(username);
191
192 if (pwdCredential == null)
193 {
194 return null;
195 }
196
197 pwdCredential = convertCredentialLdapToJetty(pwdCredential);
198 Credential credential = Credential.getCredential(pwdCredential);
199 List roles = getUserRoles(_rootContext, username);
200
201 return new UserInfo(username, credential, roles);
202 }
203
204 protected String doRFC2254Encoding(String inputString)
205 {
206 StringBuffer buf = new StringBuffer(inputString.length());
207 for (int i = 0; i < inputString.length(); i++)
208 {
209 char c = inputString.charAt(i);
210 switch (c)
211 {
212 case '\\':
213 buf.append("\\5c");
214 break;
215 case '*':
216 buf.append("\\2a");
217 break;
218 case '(':
219 buf.append("\\28");
220 break;
221 case ')':
222 buf.append("\\29");
223 break;
224 case '\0':
225 buf.append("\\00");
226 break;
227 default:
228 buf.append(c);
229 break;
230 }
231 }
232 return buf.toString();
233 }
234
235
236
237
238
239
240
241
242
243
244 private String getUserCredentials(String username) throws LoginException
245 {
246 String ldapCredential = null;
247
248 SearchControls ctls = new SearchControls();
249 ctls.setCountLimit(1);
250 ctls.setDerefLinkFlag(true);
251 ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
252
253 String filter = "(&(objectClass={0})({1}={2}))";
254
255 Log.debug("Searching for users with filter: \'" + filter + "\'" + " from base dn: " + _userBaseDn);
256
257 try
258 {
259 Object[] filterArguments = {_userObjectClass, _userIdAttribute, username};
260 NamingEnumeration results = _rootContext.search(_userBaseDn, filter, filterArguments, ctls);
261
262 Log.debug("Found user?: " + results.hasMoreElements());
263
264 if (!results.hasMoreElements())
265 {
266 throw new LoginException("User not found.");
267 }
268
269 SearchResult result = findUser(username);
270
271 Attributes attributes = result.getAttributes();
272
273 Attribute attribute = attributes.get(_userPasswordAttribute);
274 if (attribute != null)
275 {
276 try
277 {
278 byte[] value = (byte[]) attribute.get();
279
280 ldapCredential = new String(value);
281 }
282 catch (NamingException e)
283 {
284 Log.debug("no password available under attribute: " + _userPasswordAttribute);
285 }
286 }
287 }
288 catch (NamingException e)
289 {
290 throw new LoginException("Root context binding failure.");
291 }
292
293 Log.debug("user cred is: " + ldapCredential);
294
295 return ldapCredential;
296 }
297
298
299
300
301
302
303
304
305
306
307
308 private List getUserRoles(DirContext dirContext, String username) throws LoginException, NamingException
309 {
310 String userDn = _userRdnAttribute + "=" + username + "," + _userBaseDn;
311
312 return getUserRolesByDn(dirContext, userDn);
313 }
314
315 private List getUserRolesByDn(DirContext dirContext, String userDn) throws LoginException, NamingException
316 {
317 ArrayList roleList = new ArrayList();
318
319 if (dirContext == null || _roleBaseDn == null || _roleMemberAttribute == null || _roleObjectClass == null)
320 {
321 return roleList;
322 }
323
324 SearchControls ctls = new SearchControls();
325 ctls.setDerefLinkFlag(true);
326 ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
327
328 String filter = "(&(objectClass={0})({1}={2}))";
329 Object[] filterArguments = {_roleObjectClass, _roleMemberAttribute, userDn};
330 NamingEnumeration results = dirContext.search(_roleBaseDn, filter, filterArguments, ctls);
331
332 Log.debug("Found user roles?: " + results.hasMoreElements());
333
334 while (results.hasMoreElements())
335 {
336 SearchResult result = (SearchResult) results.nextElement();
337
338 Attributes attributes = result.getAttributes();
339
340 if (attributes == null)
341 {
342 continue;
343 }
344
345 Attribute roleAttribute = attributes.get(_roleNameAttribute);
346
347 if (roleAttribute == null)
348 {
349 continue;
350 }
351
352 NamingEnumeration roles = roleAttribute.getAll();
353 while (roles.hasMore())
354 {
355 roleList.add(roles.next());
356 }
357 }
358
359 return roleList;
360 }
361
362
363
364
365
366
367
368
369
370
371
372
373 public boolean login() throws LoginException
374 {
375 try
376 {
377 if (getCallbackHandler() == null)
378 {
379 throw new LoginException("No callback handler");
380 }
381
382 Callback[] callbacks = configureCallbacks();
383 getCallbackHandler().handle(callbacks);
384
385 String webUserName = ((NameCallback) callbacks[0]).getName();
386 Object webCredential = ((ObjectCallback) callbacks[1]).getObject();
387
388 if (webUserName == null || webCredential == null)
389 {
390 setAuthenticated(false);
391 return isAuthenticated();
392 }
393
394 if (_forceBindingLogin)
395 {
396 return bindingLogin(webUserName, webCredential);
397 }
398
399
400 UserInfo userInfo = getUserInfo(webUserName);
401
402 if (userInfo == null)
403 {
404 setAuthenticated(false);
405 return false;
406 }
407
408 setCurrentUser(new JAASUserInfo(userInfo));
409
410 if (webCredential instanceof String)
411 {
412 return credentialLogin(Credential.getCredential((String) webCredential));
413 }
414
415 return credentialLogin(webCredential);
416 }
417 catch (UnsupportedCallbackException e)
418 {
419 throw new LoginException("Error obtaining callback information.");
420 }
421 catch (IOException e)
422 {
423 if (_debug)
424 {
425 e.printStackTrace();
426 }
427 throw new LoginException("IO Error performing login.");
428 }
429 catch (Exception e)
430 {
431 if (_debug)
432 {
433 e.printStackTrace();
434 }
435 throw new LoginException("Error obtaining user info.");
436 }
437 }
438
439
440
441
442
443
444
445
446 protected boolean credentialLogin(Object webCredential) throws LoginException
447 {
448 setAuthenticated(getCurrentUser().checkCredential(webCredential));
449 return isAuthenticated();
450 }
451
452
453
454
455
456
457
458
459
460
461
462
463 public boolean bindingLogin(String username, Object password) throws LoginException, NamingException
464 {
465 SearchResult searchResult = findUser(username);
466
467 String userDn = searchResult.getNameInNamespace();
468
469 Log.info("Attempting authentication: " + userDn);
470
471 Hashtable environment = getEnvironment();
472 environment.put(Context.SECURITY_PRINCIPAL, userDn);
473 environment.put(Context.SECURITY_CREDENTIALS, password);
474
475 DirContext dirContext = new InitialDirContext(environment);
476 List roles = getUserRolesByDn(dirContext, userDn);
477
478 UserInfo userInfo = new UserInfo(username, null, roles);
479 setCurrentUser(new JAASUserInfo(userInfo));
480 setAuthenticated(true);
481
482 return true;
483 }
484
485 private SearchResult findUser(String username) throws NamingException, LoginException
486 {
487 SearchControls ctls = new SearchControls();
488 ctls.setCountLimit(1);
489 ctls.setDerefLinkFlag(true);
490 ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
491
492 String filter = "(&(objectClass={0})({1}={2}))";
493
494 Log.info("Searching for users with filter: \'" + filter + "\'" + " from base dn: " + _userBaseDn);
495
496 Object[] filterArguments = new Object[]{
497 _userObjectClass,
498 _userIdAttribute,
499 username
500 };
501 NamingEnumeration results = _rootContext.search(_userBaseDn, filter, filterArguments, ctls);
502
503 Log.info("Found user?: " + results.hasMoreElements());
504
505 if (!results.hasMoreElements())
506 {
507 throw new LoginException("User not found.");
508 }
509
510 return (SearchResult) results.nextElement();
511 }
512
513
514
515
516
517
518
519
520
521
522
523 public void initialize(Subject subject,
524 CallbackHandler callbackHandler,
525 Map sharedState,
526 Map options)
527 {
528 super.initialize(subject, callbackHandler, sharedState, options);
529
530 _hostname = (String) options.get("hostname");
531 _port = Integer.parseInt((String) options.get("port"));
532 _contextFactory = (String) options.get("contextFactory");
533 _bindDn = (String) options.get("bindDn");
534 _bindPassword = (String) options.get("bindPassword");
535 _authenticationMethod = (String) options.get("authenticationMethod");
536
537 _userBaseDn = (String) options.get("userBaseDn");
538
539 _roleBaseDn = (String) options.get("roleBaseDn");
540
541 if (options.containsKey("forceBindingLogin"))
542 {
543 _forceBindingLogin = Boolean.parseBoolean((String) options.get("forceBindingLogin"));
544 }
545
546 if (options.containsKey("useLdaps"))
547 {
548 _useLdaps = Boolean.parseBoolean((String) options.get("useLdaps"));
549 }
550
551 _userObjectClass = getOption(options, "userObjectClass", _userObjectClass);
552 _userRdnAttribute = getOption(options, "userRdnAttribute", _userRdnAttribute);
553 _userIdAttribute = getOption(options, "userIdAttribute", _userIdAttribute);
554 _userPasswordAttribute = getOption(options, "userPasswordAttribute", _userPasswordAttribute);
555 _roleObjectClass = getOption(options, "roleObjectClass", _roleObjectClass);
556 _roleMemberAttribute = getOption(options, "roleMemberAttribute", _roleMemberAttribute);
557 _roleNameAttribute = getOption(options, "roleNameAttribute", _roleNameAttribute);
558 _debug = Boolean.parseBoolean(String.valueOf(getOption(options, "debug", Boolean.toString(_debug))));
559
560 try
561 {
562 _rootContext = new InitialDirContext(getEnvironment());
563 }
564 catch (NamingException ex)
565 {
566 throw new IllegalStateException("Unable to establish root context", ex);
567 }
568 }
569
570 public boolean commit() throws LoginException
571 {
572 try
573 {
574 _rootContext.close();
575 }
576 catch (NamingException e)
577 {
578 throw new LoginException( "error closing root context: " + e.getMessage() );
579 }
580
581 return super.commit();
582 }
583
584 public boolean abort() throws LoginException
585 {
586 try
587 {
588 _rootContext.close();
589 }
590 catch (NamingException e)
591 {
592 throw new LoginException( "error closing root context: " + e.getMessage() );
593 }
594
595 return super.abort();
596 }
597
598 private String getOption(Map options, String key, String defaultValue)
599 {
600 Object value = options.get(key);
601
602 if (value == null)
603 {
604 return defaultValue;
605 }
606
607 return (String) value;
608 }
609
610
611
612
613
614
615 public Hashtable<Object, Object> getEnvironment()
616 {
617 Properties env = new Properties();
618
619 env.put(Context.INITIAL_CONTEXT_FACTORY, _contextFactory);
620
621 if (_hostname != null)
622 {
623 env.put(Context.PROVIDER_URL, (_useLdaps?"ldaps://":"ldap://") + _hostname + (_port==0?"":":"+_port) +"/");
624 }
625
626 if (_authenticationMethod != null)
627 {
628 env.put(Context.SECURITY_AUTHENTICATION, _authenticationMethod);
629 }
630
631 if (_bindDn != null)
632 {
633 env.put(Context.SECURITY_PRINCIPAL, _bindDn);
634 }
635
636 if (_bindPassword != null)
637 {
638 env.put(Context.SECURITY_CREDENTIALS, _bindPassword);
639 }
640
641 return env;
642 }
643
644 public static String convertCredentialJettyToLdap(String encryptedPassword)
645 {
646 if ("MD5:".startsWith(encryptedPassword.toUpperCase()))
647 {
648 return "{MD5}" + encryptedPassword.substring("MD5:".length(), encryptedPassword.length());
649 }
650
651 if ("CRYPT:".startsWith(encryptedPassword.toUpperCase()))
652 {
653 return "{CRYPT}" + encryptedPassword.substring("CRYPT:".length(), encryptedPassword.length());
654 }
655
656 return encryptedPassword;
657 }
658
659 public static String convertCredentialLdapToJetty(String encryptedPassword)
660 {
661 if (encryptedPassword == null)
662 {
663 return encryptedPassword;
664 }
665
666 if ("{MD5}".startsWith(encryptedPassword.toUpperCase()))
667 {
668 return "MD5:" + encryptedPassword.substring("{MD5}".length(), encryptedPassword.length());
669 }
670
671 if ("{CRYPT}".startsWith(encryptedPassword.toUpperCase()))
672 {
673 return "CRYPT:" + encryptedPassword.substring("{CRYPT}".length(), encryptedPassword.length());
674 }
675
676 return encryptedPassword;
677 }
678 }