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