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