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