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