View Javadoc

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