View Javadoc

1   package org.mortbay.jetty.plus.jaas.ldap;
2   
3   // ========================================================================
4   // Copyright 2007 Mort Bay Consulting Pty. Ltd.
5   // ------------------------------------------------------------------------
6   // Licensed under the Apache License, Version 2.0 (the "License");
7   // you may not use this file except in compliance with the License.
8   // You may obtain a copy of the License at
9   // http://www.apache.org/licenses/LICENSE-2.0
10  // Unless required by applicable law or agreed to in writing, software
11  // distributed under the License is distributed on an "AS IS" BASIS,
12  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  // See the License for the specific language governing permissions and
14  // limitations under the License.
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   * A LdapLoginModule for use with JAAS setups
49   *
50   * The jvm should be started with the following parameter:
51   * <br><br>
52   * <code>
53   * -Djava.security.auth.login.config=etc/ldap-loginModule.conf
54   * </code>
55   * <br><br>
56   * and an example of the ldap-loginModule.conf would be:
57   * <br><br>
58   * <pre>
59   * ldaploginmodule {
60   *    org.mortbay.jetty.plus.jaas.spi.LdapLoginModule required
61   *    debug="true"
62   *    contextFactory="com.sun.jndi.ldap.LdapCtxFactory"
63   *    hostname="ldap.example.com"
64   *    port="389"
65   *    bindDn="cn=Directory Manager"
66   *    bindPassword="directory"
67   *    authenticationMethod="simple"
68   *    forceBindingLogin="false"
69   *    userBaseDn="ou=people,dc=alcatel"
70   *    userRdnAttribute="uid"
71   *    userIdAttribute="uid"
72   *    userPasswordAttribute="userPassword"
73   *    userObjectClass="inetOrgPerson"
74   *    roleBaseDn="ou=groups,dc=example,dc=com"
75   *    roleNameAttribute="cn"
76   *    roleMemberAttribute="uniqueMember"
77   *    roleObjectClass="groupOfUniqueNames";
78   *    };
79   *  </pre>
80   *
81   * @author Jesse McConnell <jesse@codehaus.org>
82   * @author Frederic Nizery <frederic.nizery@alcatel-lucent.fr>
83   * @author Trygve Laugstol <trygvis@codehaus.org>
84   */
85  public class LdapLoginModule extends AbstractLoginModule
86  {
87      /**
88       * hostname of the ldap server
89       */
90      private String _hostname;
91  
92      /**
93       * port of the ldap server
94       */
95      private int _port;
96  
97      /**
98       * Context.SECURITY_AUTHENTICATION
99       */
100     private String _authenticationMethod;
101 
102     /**
103      * Context.INITIAL_CONTEXT_FACTORY
104      */
105     private String _contextFactory;
106 
107     /**
108      * root DN used to connect to
109      */
110     private String _bindDn;
111 
112     /**
113      * password used to connect to the root ldap context
114      */
115     private String _bindPassword;
116 
117     /**
118      * object class of a user
119      */
120     private String _userObjectClass = "inetOrgPerson";
121 
122     /**
123      * attribute that the principal is located
124      */
125     private String _userRdnAttribute = "uid";
126 
127     /**
128      * attribute that the principal is located
129      */
130     private String _userIdAttribute = "cn";
131 
132     /**
133      * name of the attribute that a users password is stored under
134      * <p/>
135      * NOTE: not always accessible, see force binding login
136      */
137     private String _userPasswordAttribute = "userPassword";
138 
139     /**
140      * base DN where users are to be searched from
141      */
142     private String _userBaseDn;
143 
144     /**
145      * base DN where role membership is to be searched from
146      */
147     private String _roleBaseDn;
148 
149     /**
150      * object class of roles
151      */
152     private String _roleObjectClass = "groupOfUniqueNames";
153 
154     /**
155      * name of the attribute that a username would be under a role class
156      */
157     private String _roleMemberAttribute = "uniqueMember";
158 
159     /**
160      * the name of the attribute that a role would be stored under
161      */
162     private String _roleNameAttribute = "roleName";
163 
164     private boolean _debug;
165 
166     /**
167      * if the getUserInfo can pull a password off of the user then
168      * password comparison is an option for authn, to force binding
169      * login checks, set this to true
170      */
171     private boolean _forceBindingLogin = false;
172 
173     private DirContext _rootContext;
174 
175     /**
176      * get the available information about the user
177      * <p/>
178      * for this LoginModule, the credential can be null which will result in a
179      * binding ldap authentication scenario
180      * <p/>
181      * roles are also an optional concept if required
182      *
183      * @param username
184      * @return
185      * @throws Exception
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         //String md5Credential = Credential.MD5.digest("foo");
199         //byte[] ba = digestMD5("foo");
200         //System.out.println(md5Credential + "  " + ba );
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      * attempts to get the users credentials from the users context
240      * <p/>
241      * NOTE: this is not an user authenticated operation
242      *
243      * @param username
244      * @return
245      * @throws LoginException
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      * attempts to get the users roles from the root context
303      * <p/>
304      * NOTE: this is not an user authenticated operation
305      *
306      * @param dirContext
307      * @param username
308      * @return
309      * @throws LoginException
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      * since ldap uses a context bind for valid authentication checking, we override login()
367      * <p/>
368      * if credentials are not available from the users context or if we are forcing the binding check
369      * then we try a binding authentication check, otherwise if we have the users encoded password then
370      * we can try authentication via that mechanic
371      *
372      * @return
373      * @throws LoginException
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             // This sets read and the credential
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      * password supplied authentication check
442      *
443      * @param webCredential
444      * @return
445      * @throws LoginException
446      */
447     protected boolean credentialLogin(Object webCredential) throws LoginException
448     {
449         setAuthenticated(getCurrentUser().checkCredential(webCredential));
450         return isAuthenticated();
451     }
452 
453     /**
454      * binding authentication check
455      * This methode of authentication works only if the user branch of the DIT (ldap tree)
456      * has an ACI (acces control instruction) that allow the access to any user or at least
457      * for the user that logs in.
458      *
459      * @param username
460      * @param password
461      * @return
462      * @throws LoginException
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      * get the context for connection
602      *
603      * @return
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 }