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   *    useLdaps="false"
63   *    contextFactory="com.sun.jndi.ldap.LdapCtxFactory"
64   *    hostname="ldap.example.com"
65   *    port="389"
66   *    bindDn="cn=Directory Manager"
67   *    bindPassword="directory"
68   *    authenticationMethod="simple"
69   *    forceBindingLogin="false"
70   *    userBaseDn="ou=people,dc=alcatel"
71   *    userRdnAttribute="uid"
72   *    userIdAttribute="uid"
73   *    userPasswordAttribute="userPassword"
74   *    userObjectClass="inetOrgPerson"
75   *    roleBaseDn="ou=groups,dc=example,dc=com"
76   *    roleNameAttribute="cn"
77   *    roleMemberAttribute="uniqueMember"
78   *    roleObjectClass="groupOfUniqueNames";
79   *    };
80   *  </pre>
81   *
82   * @author Jesse McConnell <jesse@codehaus.org>
83   * @author Frederic Nizery <frederic.nizery@alcatel-lucent.fr>
84   * @author Trygve Laugstol <trygvis@codehaus.org>
85   */
86  public class LdapLoginModule extends AbstractLoginModule
87  {
88      /**
89       * hostname of the ldap server
90       */
91      private String _hostname;
92  
93      /**
94       * port of the ldap server
95       */
96      private int _port;
97  
98      /**
99       * Context.SECURITY_AUTHENTICATION
100      */
101     private String _authenticationMethod;
102 
103     /**
104      * Context.INITIAL_CONTEXT_FACTORY
105      */
106     private String _contextFactory;
107 
108     /**
109      * root DN used to connect to
110      */
111     private String _bindDn;
112 
113     /**
114      * password used to connect to the root ldap context
115      */
116     private String _bindPassword;
117 
118     /**
119      * object class of a user
120      */
121     private String _userObjectClass = "inetOrgPerson";
122 
123     /**
124      * attribute that the principal is located
125      */
126     private String _userRdnAttribute = "uid";
127 
128     /**
129      * attribute that the principal is located
130      */
131     private String _userIdAttribute = "cn";
132 
133     /**
134      * name of the attribute that a users password is stored under
135      * <p/>
136      * NOTE: not always accessible, see force binding login
137      */
138     private String _userPasswordAttribute = "userPassword";
139 
140     /**
141      * base DN where users are to be searched from
142      */
143     private String _userBaseDn;
144 
145     /**
146      * base DN where role membership is to be searched from
147      */
148     private String _roleBaseDn;
149 
150     /**
151      * object class of roles
152      */
153     private String _roleObjectClass = "groupOfUniqueNames";
154 
155     /**
156      * name of the attribute that a username would be under a role class
157      */
158     private String _roleMemberAttribute = "uniqueMember";
159 
160     /**
161      * the name of the attribute that a role would be stored under
162      */
163     private String _roleNameAttribute = "roleName";
164 
165     private boolean _debug;
166 
167     /**
168      * if the getUserInfo can pull a password off of the user then
169      * password comparison is an option for authn, to force binding
170      * login checks, set this to true
171      */
172     private boolean _forceBindingLogin = false;
173     
174     /**
175      * When true changes the protocol to ldaps
176      */
177     private boolean _useLdaps = false;
178 
179     private DirContext _rootContext;
180 
181     /**
182      * get the available information about the user
183      * <p/>
184      * for this LoginModule, the credential can be null which will result in a
185      * binding ldap authentication scenario
186      * <p/>
187      * roles are also an optional concept if required
188      *
189      * @param username
190      * @return
191      * @throws Exception
192      */
193     public UserInfo getUserInfo(String username) throws Exception
194     {
195         String pwdCredential = getUserCredentials(username);
196 
197         if (pwdCredential == null)
198         {
199             return null;
200         }
201 
202         pwdCredential = convertCredentialLdapToJetty(pwdCredential);
203         Credential credential = Credential.getCredential(pwdCredential);
204         List roles = getUserRoles(_rootContext, username);
205 
206         return new UserInfo(username, credential, roles);
207     }
208 
209     protected String doRFC2254Encoding(String inputString)
210     {
211         StringBuffer buf = new StringBuffer(inputString.length());
212         for (int i = 0; i < inputString.length(); i++)
213         {
214             char c = inputString.charAt(i);
215             switch (c)
216             {
217                 case '\\':
218                     buf.append("\\5c");
219                     break;
220                 case '*':
221                     buf.append("\\2a");
222                     break;
223                 case '(':
224                     buf.append("\\28");
225                     break;
226                 case ')':
227                     buf.append("\\29");
228                     break;
229                 case '\0':
230                     buf.append("\\00");
231                     break;
232                 default:
233                     buf.append(c);
234                     break;
235             }
236         }
237         return buf.toString();
238     }
239 
240     /**
241      * attempts to get the users credentials from the users context
242      * <p/>
243      * NOTE: this is not an user authenticated operation
244      *
245      * @param username
246      * @return
247      * @throws LoginException
248      */
249     private String getUserCredentials(String username) throws LoginException
250     {
251         String ldapCredential = null;
252 
253         SearchControls ctls = new SearchControls();
254         ctls.setCountLimit(1);
255         ctls.setDerefLinkFlag(true);
256         ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
257 
258         String filter = "(&(objectClass={0})({1}={2}))";
259 
260         Log.debug("Searching for users with filter: \'" + filter + "\'" + " from base dn: " + _userBaseDn);
261 
262         try
263         {
264             Object[] filterArguments = {_userObjectClass, _userIdAttribute, username};
265             NamingEnumeration results = _rootContext.search(_userBaseDn, filter, filterArguments, ctls);
266 
267             Log.debug("Found user?: " + results.hasMoreElements());
268 
269             if (!results.hasMoreElements())
270             {
271                 throw new LoginException("User not found.");
272             }
273 
274             SearchResult result = findUser(username);
275 
276             Attributes attributes = result.getAttributes();
277 
278             Attribute attribute = attributes.get(_userPasswordAttribute);
279             if (attribute != null)
280             {
281                 try
282                 {
283                     byte[] value = (byte[]) attribute.get();
284 
285                     ldapCredential = new String(value);
286                 }
287                 catch (NamingException e)
288                 {
289                     Log.debug("no password available under attribute: " + _userPasswordAttribute);
290                 }
291             }
292         }
293         catch (NamingException e)
294         {
295             throw new LoginException("Root context binding failure.");
296         }
297 
298         Log.debug("user cred is: " + ldapCredential);
299 
300         return ldapCredential;
301     }
302 
303     /**
304      * attempts to get the users roles from the root context
305      * <p/>
306      * NOTE: this is not an user authenticated operation
307      *
308      * @param dirContext
309      * @param username
310      * @return
311      * @throws LoginException
312      */
313     private List getUserRoles(DirContext dirContext, String username) throws LoginException, NamingException
314     {
315         String userDn = _userRdnAttribute + "=" + username + "," + _userBaseDn;
316 
317         return getUserRolesByDn(dirContext, userDn);
318     }
319 
320     private List getUserRolesByDn(DirContext dirContext, String userDn) throws LoginException, NamingException
321     {
322         ArrayList roleList = new ArrayList();
323 
324         if (dirContext == null || _roleBaseDn == null || _roleMemberAttribute == null || _roleObjectClass == null)
325         {
326             return roleList;
327         }
328 
329         SearchControls ctls = new SearchControls();
330         ctls.setDerefLinkFlag(true);
331         ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
332 
333         String filter = "(&(objectClass={0})({1}={2}))";
334         Object[] filterArguments = {_roleObjectClass, _roleMemberAttribute, userDn};
335         NamingEnumeration results = dirContext.search(_roleBaseDn, filter, filterArguments, ctls);
336 
337         Log.debug("Found user roles?: " + results.hasMoreElements());
338 
339         while (results.hasMoreElements())
340         {
341             SearchResult result = (SearchResult)results.nextElement();
342 
343             Attributes attributes = result.getAttributes();
344 
345             if (attributes == null)
346             {
347                 continue;
348             }
349 
350             Attribute roleAttribute = attributes.get(_roleNameAttribute);
351 
352             if (roleAttribute == null)
353             {
354                 continue;
355             }
356 
357             NamingEnumeration roles = roleAttribute.getAll();
358             while (roles.hasMore())
359             {
360                 roleList.add(roles.next());
361             }
362         }
363 
364         return roleList;
365     }
366 
367     /**
368      * since ldap uses a context bind for valid authentication checking, we override login()
369      * <p/>
370      * if credentials are not available from the users context or if we are forcing the binding check
371      * then we try a binding authentication check, otherwise if we have the users encoded password then
372      * we can try authentication via that mechanic
373      *
374      * @return
375      * @throws LoginException
376      */
377     public boolean login() throws LoginException
378     {
379         try
380         {
381             if (getCallbackHandler() == null)
382             {
383                 throw new LoginException("No callback handler");
384             }
385 
386             Callback[] callbacks = configureCallbacks();
387             getCallbackHandler().handle(callbacks);
388 
389             String webUserName = ((NameCallback) callbacks[0]).getName();
390             Object webCredential = ((ObjectCallback) callbacks[1]).getObject();
391 
392             if (webUserName == null || webCredential == null)
393             {
394                 setAuthenticated(false);
395                 return isAuthenticated();
396             }
397 
398             if (_forceBindingLogin)
399             {
400                 return bindingLogin(webUserName, webCredential);
401             }
402 
403             // This sets read and the credential
404             UserInfo userInfo = getUserInfo(webUserName);
405 
406             if( userInfo == null) {
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      * password supplied authentication check
444      *
445      * @param webCredential
446      * @return
447      * @throws LoginException
448      */
449     protected boolean credentialLogin(Object webCredential) throws LoginException
450     {
451         setAuthenticated(getCurrentUser().checkCredential(webCredential));
452         return isAuthenticated();
453     }
454 
455     /**
456      * binding authentication check
457      * This methode of authentication works only if the user branch of the DIT (ldap tree)
458      * has an ACI (acces control instruction) that allow the access to any user or at least
459      * for the user that logs in.
460      *
461      * @param username
462      * @param password
463      * @return
464      * @throws LoginException
465      */
466     protected boolean bindingLogin(String username, Object password) throws LoginException, NamingException
467     {
468         SearchResult searchResult = findUser(username);
469 
470         DirContext usrsContext = (DirContext)_rootContext.lookup(_userBaseDn);
471         DirContext usrContext = (DirContext)usrsContext.lookup(searchResult.getName());
472         String userDn = usrContext.getNameInNamespace();
473 
474         Log.info("Attempting authentication: " + userDn);
475 
476         Hashtable environment = getEnvironment();
477         environment.put(Context.SECURITY_PRINCIPAL, userDn);
478         environment.put(Context.SECURITY_CREDENTIALS, password);
479 
480         DirContext dirContext = new InitialDirContext(environment);
481 
482         List roles = getUserRolesByDn(dirContext, userDn);
483 
484         UserInfo userInfo = new UserInfo(username, null, roles);
485         setCurrentUser(new JAASUserInfo(userInfo));
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         if (options.containsKey("useLdaps"))
543         {
544             _useLdaps = Boolean.parseBoolean((String) options.get("useLdaps"));
545         }     
546         
547         _userObjectClass = getOption(options, "userObjectClass", _userObjectClass);
548         _userRdnAttribute = getOption(options, "userRdnAttribute", _userRdnAttribute);
549         _userIdAttribute = getOption(options, "userIdAttribute", _userIdAttribute);
550         _userPasswordAttribute = getOption(options, "userPasswordAttribute", _userPasswordAttribute);
551         _roleObjectClass = getOption(options, "roleObjectClass", _roleObjectClass);
552         _roleMemberAttribute = getOption(options, "roleMemberAttribute", _roleMemberAttribute);
553         _roleNameAttribute = getOption(options, "roleNameAttribute", _roleNameAttribute);
554         _debug = Boolean.valueOf(String.valueOf(getOption(options, "debug", Boolean.toString(_debug)))).booleanValue();
555 
556         try
557         {
558             _rootContext = new InitialDirContext(getEnvironment());
559         }
560         catch (NamingException ex)
561         {
562             throw new RuntimeException("Unable to establish root context", ex);
563         }
564     }
565 
566     public boolean commit() throws LoginException 
567     {
568         try 
569         {
570             _rootContext.close();
571         } 
572         catch (NamingException e) 
573         {
574             throw new LoginException("error closing root context: " + e.getMessage());
575         }
576 
577         return super.commit();
578     }
579 
580     public boolean abort() throws LoginException 
581     {
582         try 
583         {
584             _rootContext.close();
585         } 
586         catch (NamingException e) 
587         {
588             throw new LoginException("error closing root context: " + e.getMessage());
589         }
590 
591         return super.abort();
592     }
593 
594     private String getOption(Map options, String key, String defaultValue)
595     {
596         Object value = options.get(key);
597 
598         if (value == null) {
599             return defaultValue;
600         }
601 
602         return (String) value;
603     }
604 
605     /**
606      * get the context for connection
607      *
608      * @return
609      */
610     public Hashtable getEnvironment()
611     {
612         Properties env = new Properties();
613 
614         env.put(Context.INITIAL_CONTEXT_FACTORY, _contextFactory);
615 
616         if (_hostname != null)
617         {
618             env.put(Context.PROVIDER_URL, (_useLdaps?"ldaps://":"ldap://") + _hostname + (_port==0?"":":"+_port) +"/");
619         }
620 
621         if (_authenticationMethod != null)
622         {
623             env.put(Context.SECURITY_AUTHENTICATION, _authenticationMethod);
624         }
625 
626         if (_bindDn != null)
627         {
628             env.put(Context.SECURITY_PRINCIPAL, _bindDn);
629         }
630 
631         if (_bindPassword != null)
632         {
633             env.put(Context.SECURITY_CREDENTIALS, _bindPassword);
634         }
635 
636         return env;
637     }
638 
639     public static String convertCredentialJettyToLdap( String encryptedPassword )
640     {
641         if ("MD5:".startsWith(encryptedPassword.toUpperCase()))
642         {
643             return "{MD5}" + encryptedPassword.substring("MD5:".length(), encryptedPassword.length());
644         }
645 
646         if ("CRYPT:".startsWith(encryptedPassword.toUpperCase()))
647         {
648             return "{CRYPT}" + encryptedPassword.substring("CRYPT:".length(), encryptedPassword.length());
649         }
650 
651         return encryptedPassword;
652     }
653 
654     public static String convertCredentialLdapToJetty( String encryptedPassword )
655     {
656         if (encryptedPassword == null)
657         {
658             return encryptedPassword;
659         }
660 
661         if ("{MD5}".startsWith(encryptedPassword.toUpperCase()))
662         {
663             return "MD5:" + encryptedPassword.substring("{MD5}".length(), encryptedPassword.length());
664         }
665 
666         if ("{CRYPT}".startsWith(encryptedPassword.toUpperCase()))
667         {
668             return "CRYPT:" + encryptedPassword.substring("{CRYPT}".length(), encryptedPassword.length());
669         }
670 
671         return encryptedPassword;
672     }
673 }