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