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