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 public class LdapLoginModule extends AbstractLoginModule
86 {
87
88
89
90 private String _hostname;
91
92
93
94
95 private int _port;
96
97
98
99
100 private String _authenticationMethod;
101
102
103
104
105 private String _contextFactory;
106
107
108
109
110 private String _bindDn;
111
112
113
114
115 private String _bindPassword;
116
117
118
119
120 private String _userObjectClass = "inetOrgPerson";
121
122
123
124
125 private String _userRdnAttribute = "uid";
126
127
128
129
130 private String _userIdAttribute = "cn";
131
132
133
134
135
136
137 private String _userPasswordAttribute = "userPassword";
138
139
140
141
142 private String _userBaseDn;
143
144
145
146
147 private String _roleBaseDn;
148
149
150
151
152 private String _roleObjectClass = "groupOfUniqueNames";
153
154
155
156
157 private String _roleMemberAttribute = "uniqueMember";
158
159
160
161
162 private String _roleNameAttribute = "roleName";
163
164 private boolean _debug;
165
166
167
168
169
170
171 private boolean _forceBindingLogin = false;
172
173 private DirContext _rootContext;
174
175
176
177
178
179
180
181
182
183
184
185
186
187 public UserInfo getUserInfo(String username) throws Exception
188 {
189 String pwdCredential = getUserCredentials(username);
190
191 if (pwdCredential == null)
192 {
193 return null;
194 }
195
196 pwdCredential = convertCredentialLdapToJetty(pwdCredential);
197
198
199
200
201 Credential credential = Credential.getCredential(pwdCredential);
202 List 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 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 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 getUserRolesByDn(DirContext dirContext, String userDn) throws LoginException, NamingException
319 {
320 ArrayList roleList = new ArrayList();
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
331 String filter = "(&(objectClass={0})({1}={2}))";
332 Object[] filterArguments = {_roleObjectClass, _roleMemberAttribute, userDn};
333 NamingEnumeration results = dirContext.search(_roleBaseDn, filter, filterArguments, ctls);
334
335 Log.debug("Found user roles?: " + results.hasMoreElements());
336
337 while (results.hasMoreElements())
338 {
339 SearchResult result = (SearchResult)results.nextElement();
340
341 Attributes attributes = result.getAttributes();
342
343 if (attributes == null)
344 {
345 continue;
346 }
347
348 Attribute roleAttribute = attributes.get(_roleNameAttribute);
349
350 if (roleAttribute == null)
351 {
352 continue;
353 }
354
355 NamingEnumeration roles = roleAttribute.getAll();
356 while (roles.hasMore())
357 {
358 roleList.add(roles.next());
359 }
360 }
361
362 return roleList;
363 }
364
365
366
367
368
369
370
371
372
373
374
375 public boolean login() throws LoginException
376 {
377 try
378 {
379 if (getCallbackHandler() == null)
380 {
381 throw new LoginException("No callback handler");
382 }
383
384 Callback[] callbacks = configureCallbacks();
385 getCallbackHandler().handle(callbacks);
386
387 String webUserName = ((NameCallback) callbacks[0]).getName();
388 Object webCredential = ((ObjectCallback) callbacks[1]).getObject();
389
390 if (webUserName == null || webCredential == null)
391 {
392 setAuthenticated(false);
393 return isAuthenticated();
394 }
395
396 if (_forceBindingLogin)
397 {
398 return bindingLogin(webUserName, webCredential);
399 }
400
401
402 UserInfo userInfo = getUserInfo(webUserName);
403
404 if( userInfo == null) {
405 setAuthenticated(false);
406 return false;
407 }
408
409 setCurrentUser(new JAASUserInfo(userInfo));
410
411 if (webCredential instanceof String)
412 {
413 return credentialLogin(Credential.getCredential((String) webCredential));
414 }
415
416 return credentialLogin(webCredential);
417 }
418 catch (UnsupportedCallbackException e)
419 {
420 throw new LoginException("Error obtaining callback information.");
421 }
422 catch (IOException e)
423 {
424 if (_debug)
425 {
426 e.printStackTrace();
427 }
428 throw new LoginException("IO Error performing login.");
429 }
430 catch (Exception e)
431 {
432 if (_debug)
433 {
434 e.printStackTrace();
435 }
436 throw new LoginException("Error obtaining user info.");
437 }
438 }
439
440
441
442
443
444
445
446
447 protected boolean credentialLogin(Object webCredential) throws LoginException
448 {
449 setAuthenticated(getCurrentUser().checkCredential(webCredential));
450 return isAuthenticated();
451 }
452
453
454
455
456
457
458
459
460
461
462
463
464 protected boolean bindingLogin(String username, Object password) throws LoginException, NamingException
465 {
466 SearchResult searchResult = findUser(username);
467
468 DirContext usrsContext = (DirContext)_rootContext.lookup(_userBaseDn);
469 DirContext usrContext = (DirContext)usrsContext.lookup(searchResult.getName());
470 String userDn = usrContext.getNameInNamespace();
471
472 Log.info("Attempting authentication: " + userDn);
473
474 Hashtable environment = getEnvironment();
475 environment.put(Context.SECURITY_PRINCIPAL, userDn);
476 environment.put(Context.SECURITY_CREDENTIALS, password);
477
478 DirContext dirContext = new InitialDirContext(environment);
479
480 List roles = getUserRolesByDn(dirContext, userDn);
481
482 UserInfo userInfo = new UserInfo(username, null, roles);
483
484 setCurrentUser(new JAASUserInfo(userInfo));
485
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 _userObjectClass = getOption(options, "userObjectClass", _userObjectClass);
543 _userRdnAttribute = getOption(options, "userRdnAttribute", _userRdnAttribute);
544 _userIdAttribute = getOption(options, "userIdAttribute", _userIdAttribute);
545 _userPasswordAttribute = getOption(options, "userPasswordAttribute", _userPasswordAttribute);
546 _roleObjectClass = getOption(options, "roleObjectClass", _roleObjectClass);
547 _roleMemberAttribute = getOption(options, "roleMemberAttribute", _roleMemberAttribute);
548 _roleNameAttribute = getOption(options, "roleNameAttribute", _roleNameAttribute);
549 _debug = Boolean.valueOf(String.valueOf(getOption(options, "debug", Boolean.toString(_debug)))).booleanValue();
550
551 try
552 {
553 _rootContext = new InitialDirContext(getEnvironment());
554 }
555 catch (NamingException ex)
556 {
557 throw new RuntimeException("Unable to establish root context", ex);
558 }
559 }
560
561 public boolean commit() throws LoginException
562 {
563 try
564 {
565 _rootContext.close();
566 }
567 catch (NamingException e)
568 {
569 throw new LoginException("error closing root context: " + e.getMessage());
570 }
571
572 return super.commit();
573 }
574
575 public boolean abort() throws LoginException
576 {
577 try
578 {
579 _rootContext.close();
580 }
581 catch (NamingException e)
582 {
583 throw new LoginException("error closing root context: " + e.getMessage());
584 }
585
586 return super.abort();
587 }
588
589 private String getOption(Map options, String key, String defaultValue)
590 {
591 Object value = options.get(key);
592
593 if (value == null) {
594 return defaultValue;
595 }
596
597 return (String) value;
598 }
599
600
601
602
603
604
605 public Hashtable getEnvironment()
606 {
607 Properties env = new Properties();
608
609 env.put(Context.INITIAL_CONTEXT_FACTORY, _contextFactory);
610
611 if (_hostname != null)
612 {
613 if (_port != 0)
614 {
615 env.put(Context.PROVIDER_URL, "ldap://" + _hostname + ":" + _port + "/");
616 }
617 else
618 {
619 env.put(Context.PROVIDER_URL, "ldap://" + _hostname + "/");
620 }
621 }
622
623 if (_authenticationMethod != null)
624 {
625 env.put(Context.SECURITY_AUTHENTICATION, _authenticationMethod);
626 }
627
628 if (_bindDn != null)
629 {
630 env.put(Context.SECURITY_PRINCIPAL, _bindDn);
631 }
632
633 if (_bindPassword != null)
634 {
635 env.put(Context.SECURITY_CREDENTIALS, _bindPassword);
636 }
637
638 return env;
639 }
640
641 public static String convertCredentialJettyToLdap( String encryptedPassword )
642 {
643 if ("MD5:".startsWith(encryptedPassword.toUpperCase()))
644 {
645 return "{MD5}" + encryptedPassword.substring("MD5:".length(), encryptedPassword.length());
646 }
647
648 if ("CRYPT:".startsWith(encryptedPassword.toUpperCase()))
649 {
650 return "{CRYPT}" + encryptedPassword.substring("CRYPT:".length(), encryptedPassword.length());
651 }
652
653 return encryptedPassword;
654 }
655
656 public static String convertCredentialLdapToJetty( String encryptedPassword )
657 {
658 if (encryptedPassword == null)
659 {
660 return encryptedPassword;
661 }
662
663 if ("{MD5}".startsWith(encryptedPassword.toUpperCase()))
664 {
665 return "MD5:" + encryptedPassword.substring("{MD5}".length(), encryptedPassword.length());
666 }
667
668 if ("{CRYPT}".startsWith(encryptedPassword.toUpperCase()))
669 {
670 return "CRYPT:" + encryptedPassword.substring("{CRYPT}".length(), encryptedPassword.length());
671 }
672
673 return encryptedPassword;
674 }
675 }