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