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