1
2
3
4
5
6
7
8
9
10
11
12
13 package org.eclipse.jetty.plus.jaas.spi;
14
15 import java.io.IOException;
16 import java.util.ArrayList;
17 import java.util.Hashtable;
18 import java.util.List;
19 import java.util.Map;
20 import java.util.Properties;
21
22 import javax.naming.Context;
23 import javax.naming.NamingEnumeration;
24 import javax.naming.NamingException;
25 import javax.naming.directory.Attribute;
26 import javax.naming.directory.Attributes;
27 import javax.naming.directory.DirContext;
28 import javax.naming.directory.InitialDirContext;
29 import javax.naming.directory.SearchControls;
30 import javax.naming.directory.SearchResult;
31 import javax.security.auth.Subject;
32 import javax.security.auth.callback.Callback;
33 import javax.security.auth.callback.CallbackHandler;
34 import javax.security.auth.callback.NameCallback;
35 import javax.security.auth.callback.UnsupportedCallbackException;
36 import javax.security.auth.login.LoginException;
37
38 import org.eclipse.jetty.http.security.Credential;
39 import org.eclipse.jetty.plus.jaas.callback.ObjectCallback;
40 import org.eclipse.jetty.util.log.Log;
41 import org.eclipse.jetty.util.log.Logger;
42
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 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
331 String filter = "(&(objectClass={0})({1}={2}))";
332 Object[] filterArguments = {_roleObjectClass, _roleMemberAttribute, userDn};
333 NamingEnumeration<SearchResult> 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().toString());
359 }
360 }
361
362 return roleList;
363 }
364
365
366
367
368
369
370
371
372
373
374
375
376 public boolean login() throws LoginException
377 {
378 try
379 {
380 if (getCallbackHandler() == null)
381 {
382 throw new LoginException("No callback handler");
383 }
384
385 Callback[] callbacks = configureCallbacks();
386 getCallbackHandler().handle(callbacks);
387
388 String webUserName = ((NameCallback) callbacks[0]).getName();
389 Object webCredential = ((ObjectCallback) callbacks[1]).getObject();
390
391 if (webUserName == null || webCredential == null)
392 {
393 setAuthenticated(false);
394 return isAuthenticated();
395 }
396
397 if (_forceBindingLogin)
398 {
399 return bindingLogin(webUserName, webCredential);
400 }
401
402
403 UserInfo userInfo = getUserInfo(webUserName);
404
405 if (userInfo == null)
406 {
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 public boolean bindingLogin(String username, Object password) throws LoginException, NamingException
467 {
468 SearchResult searchResult = findUser(username);
469
470 String userDn = searchResult.getNameInNamespace();
471
472 LOG.info("Attempting authentication: " + userDn);
473
474 Hashtable<Object,Object> environment = getEnvironment();
475 environment.put(Context.SECURITY_PRINCIPAL, userDn);
476 environment.put(Context.SECURITY_CREDENTIALS, password);
477
478 DirContext dirContext = new InitialDirContext(environment);
479 List<String> roles = getUserRolesByDn(dirContext, userDn);
480
481 UserInfo userInfo = new UserInfo(username, null, roles);
482 setCurrentUser(new JAASUserInfo(userInfo));
483 setAuthenticated(true);
484
485 return true;
486 }
487
488 private SearchResult findUser(String username) throws NamingException, LoginException
489 {
490 SearchControls ctls = new SearchControls();
491 ctls.setCountLimit(1);
492 ctls.setDerefLinkFlag(true);
493 ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
494
495 String filter = "(&(objectClass={0})({1}={2}))";
496
497 LOG.info("Searching for users with filter: \'" + filter + "\'" + " from base dn: " + _userBaseDn);
498
499 Object[] filterArguments = new Object[]{
500 _userObjectClass,
501 _userIdAttribute,
502 username
503 };
504 NamingEnumeration<SearchResult> results = _rootContext.search(_userBaseDn, filter, filterArguments, ctls);
505
506 LOG.info("Found user?: " + results.hasMoreElements());
507
508 if (!results.hasMoreElements())
509 {
510 throw new LoginException("User not found.");
511 }
512
513 return (SearchResult) results.nextElement();
514 }
515
516
517
518
519
520
521
522
523
524
525
526 public void initialize(Subject subject,
527 CallbackHandler callbackHandler,
528 Map<String,?> sharedState,
529 Map<String,?> options)
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 if (options.containsKey("useLdaps"))
550 {
551 _useLdaps = Boolean.parseBoolean((String) options.get("useLdaps"));
552 }
553
554 _userObjectClass = getOption(options, "userObjectClass", _userObjectClass);
555 _userRdnAttribute = getOption(options, "userRdnAttribute", _userRdnAttribute);
556 _userIdAttribute = getOption(options, "userIdAttribute", _userIdAttribute);
557 _userPasswordAttribute = getOption(options, "userPasswordAttribute", _userPasswordAttribute);
558 _roleObjectClass = getOption(options, "roleObjectClass", _roleObjectClass);
559 _roleMemberAttribute = getOption(options, "roleMemberAttribute", _roleMemberAttribute);
560 _roleNameAttribute = getOption(options, "roleNameAttribute", _roleNameAttribute);
561 _debug = Boolean.parseBoolean(String.valueOf(getOption(options, "debug", Boolean.toString(_debug))));
562
563 try
564 {
565 _rootContext = new InitialDirContext(getEnvironment());
566 }
567 catch (NamingException ex)
568 {
569 throw new IllegalStateException("Unable to establish root context", ex);
570 }
571 }
572
573 public boolean commit() throws LoginException
574 {
575 try
576 {
577 _rootContext.close();
578 }
579 catch (NamingException e)
580 {
581 throw new LoginException( "error closing root context: " + e.getMessage() );
582 }
583
584 return super.commit();
585 }
586
587 public boolean abort() throws LoginException
588 {
589 try
590 {
591 _rootContext.close();
592 }
593 catch (NamingException e)
594 {
595 throw new LoginException( "error closing root context: " + e.getMessage() );
596 }
597
598 return super.abort();
599 }
600
601 private String getOption(Map<String,?> options, String key, String defaultValue)
602 {
603 Object value = options.get(key);
604
605 if (value == null)
606 {
607 return defaultValue;
608 }
609
610 return (String) value;
611 }
612
613
614
615
616
617
618 public Hashtable<Object, Object> getEnvironment()
619 {
620 Properties env = new Properties();
621
622 env.put(Context.INITIAL_CONTEXT_FACTORY, _contextFactory);
623
624 if (_hostname != null)
625 {
626 env.put(Context.PROVIDER_URL, (_useLdaps?"ldaps://":"ldap://") + _hostname + (_port==0?"":":"+_port) +"/");
627 }
628
629 if (_authenticationMethod != null)
630 {
631 env.put(Context.SECURITY_AUTHENTICATION, _authenticationMethod);
632 }
633
634 if (_bindDn != null)
635 {
636 env.put(Context.SECURITY_PRINCIPAL, _bindDn);
637 }
638
639 if (_bindPassword != null)
640 {
641 env.put(Context.SECURITY_CREDENTIALS, _bindPassword);
642 }
643
644 return env;
645 }
646
647 public static String convertCredentialJettyToLdap(String encryptedPassword)
648 {
649 if ("MD5:".startsWith(encryptedPassword.toUpperCase()))
650 {
651 return "{MD5}" + encryptedPassword.substring("MD5:".length(), encryptedPassword.length());
652 }
653
654 if ("CRYPT:".startsWith(encryptedPassword.toUpperCase()))
655 {
656 return "{CRYPT}" + encryptedPassword.substring("CRYPT:".length(), encryptedPassword.length());
657 }
658
659 return encryptedPassword;
660 }
661
662 public static String convertCredentialLdapToJetty(String encryptedPassword)
663 {
664 if (encryptedPassword == null)
665 {
666 return encryptedPassword;
667 }
668
669 if ("{MD5}".startsWith(encryptedPassword.toUpperCase()))
670 {
671 return "MD5:" + encryptedPassword.substring("{MD5}".length(), encryptedPassword.length());
672 }
673
674 if ("{CRYPT}".startsWith(encryptedPassword.toUpperCase()))
675 {
676 return "CRYPT:" + encryptedPassword.substring("{CRYPT}".length(), encryptedPassword.length());
677 }
678
679 return encryptedPassword;
680 }
681 }