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.io.UnsupportedEncodingException;
19  import java.security.MessageDigest;
20  import java.security.NoSuchAlgorithmException;
21  import java.util.ArrayList;
22  import java.util.Hashtable;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Properties;
26  
27  import javax.naming.AuthenticationException;
28  import javax.naming.Context;
29  import javax.naming.NamingEnumeration;
30  import javax.naming.NamingException;
31  import javax.naming.directory.Attribute;
32  import javax.naming.directory.Attributes;
33  import javax.naming.directory.DirContext;
34  import javax.naming.directory.InitialDirContext;
35  import javax.naming.directory.SearchControls;
36  import javax.naming.directory.SearchResult;
37  import javax.security.auth.Subject;
38  import javax.security.auth.callback.Callback;
39  import javax.security.auth.callback.CallbackHandler;
40  import javax.security.auth.callback.NameCallback;
41  import javax.security.auth.callback.UnsupportedCallbackException;
42  import javax.security.auth.login.LoginException;
43  
44  import org.mortbay.jetty.plus.jaas.callback.ObjectCallback;
45  import org.mortbay.jetty.security.Credential;
46  import org.mortbay.log.Log;
47  
48  
49  /**
50   * 
51   * A LdapLoginModule for use with JAAS setups
52   * 
53   * The jvm should be started with the following parameter:
54   * <br><br>
55   * <code>
56   * -Djava.security.auth.login.config=etc/ldap-loginModule.conf
57   * </code>
58   * <br><br>
59   * and an example of the ldap-loginModule.conf would be:
60   * <br><br>
61   * <pre>
62   * ldaploginmodule {  
63   *    org.mortbay.jetty.plus.jaas.spi.LdapLoginModule required
64   *    debug="true"
65   *    contextFactory="com.sun.jndi.ldap.LdapCtxFactory"
66   *    hostname="ldap-directory_host"
67   *    port="389"
68   *    bindDn="cn=Directory Manager"
69   *    bindPassword="directory"
70   *    authenticationMethod="simple"
71   *    forceBindingLogin="false"
72   *    userBaseDn="ou=people,dc=alcatel"
73   *    userRdnAttribute="uid"
74   *    userIdAttribute="uid"
75   *    userPasswordAttribute="userPassword"
76   *    userObjectClass="inetOrgPerson"
77   *    roleBaseDn="ou=groups,dc=alcatel"
78   *    roleNameAttribute="cn"
79   *    roleMemberAttribute="uniqueMember"
80   *    roleObjectClass="groupOfUniqueNames";
81   *    }; 
82   *  </pre>   
83   *
84   * @author Jesse McConnell <jesse@codehaus.org>
85   * @author Frederic Nizery <frederic.nizery@alcatel-lucent.fr>
86   */
87  public class LdapLoginModule extends AbstractLoginModule
88  {
89      /**
90       * hostname of the ldap server
91       */
92      private String _hostname;
93  
94      /**
95       * port of the ldap server
96       */
97      private int _port;
98  
99      /**
100      * Context.SECURITY_AUTHENTICATION
101      */
102     private String _authenticationMethod;
103 
104     /**
105      * Context.INITIAL_CONTEXT_FACTORY
106      */
107     private String _contextFactory;
108 
109     /**
110      * root DN used to connect to
111      */
112     private String _bindDn;
113 
114     /**
115      * password used to connect to the root ldap context
116      */
117     private String _bindPassword;
118 
119     /**
120      * object class of a user
121      */
122     private String _userObjectClass = "inetOrgPerson";
123 
124     /**
125      * attribute that the principal is located
126      */
127     private String _userRdnAttribute = "uid";
128 
129     /**
130      * attribute that the principal is located
131      */
132     private String _userIdAttribute = "cn";
133 
134     /**
135      * name of the attribute that a users password is stored under
136      * <p/>
137      * NOTE: not always accessible, see force binding login
138      */
139     private String _userPasswordAttribute = "userPassword";
140 
141     /**
142      * base DN where users are to be searched from
143      */
144     private String _userBaseDn;
145 
146     /**
147      * base DN where role membership is to be searched from
148      */
149     private String _roleBaseDn;
150 
151     /**
152      * object class of roles
153      */
154     private String _roleObjectClass = "groupofuniquenames";
155 
156     /**
157      * name of the attribute that a username would be under a role class
158      */
159     private String _roleMemberAttribute = "uniqueMember";
160 
161     /**
162      * the name of the attribute that a role would be stored under
163      */
164     private String _roleNameAttribute = "roleName";
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 
177     /**
178      * get the available information about the user
179      * <p/>
180      * for this LoginModule, the credential can be null which will result in a
181      * binding ldap authentication scenario
182      * <p/>
183      * roles are also an optional concept if required
184      *
185      * @param username
186      * @return
187      * @throws Exception
188      */
189     public UserInfo getUserInfo(String username) throws Exception
190     {
191         String pwdCredential = getUserCredentials(username);
192         pwdCredential = convertCredentialLdapToJetty(pwdCredential);
193 
194         //String md5Credential = Credential.MD5.digest("foo");
195         //byte[] ba = digestMD5("foo");
196         //System.out.println(md5Credential + "  " + ba );
197         Credential credential = Credential.getCredential(pwdCredential);
198         List roles = getUserRoles(username);
199 
200         return new UserInfo(username, credential, roles);
201     }
202 
203 
204     protected String doRFC2254Encoding(String inputString)
205     {
206         StringBuffer buf = new StringBuffer(inputString.length());
207         for (int i = 0; i < inputString.length(); i++)
208         {
209             char c = inputString.charAt(i);
210             switch (c)
211             {
212                 case '\\':
213                     buf.append("\\5c");
214                     break;
215                 case '*':
216                     buf.append("\\2a");
217                     break;
218                 case '(':
219                     buf.append("\\28");
220                     break;
221                 case ')':
222                     buf.append("\\29");
223                     break;
224                 case '\0':
225                     buf.append("\\00");
226                     break;
227                 default:
228                     buf.append(c);
229                     break;
230             }
231         }
232         return buf.toString();
233     }
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 
248         String ldapCredential = null;
249 
250         SearchControls ctls = new SearchControls();
251 
252         ctls.setCountLimit(1);
253 
254         ctls.setDerefLinkFlag(true);
255         ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
256 
257         String filter = "(&(objectClass=" + _userObjectClass + ")(" + _userIdAttribute + "=" + username + "))";
258 
259         Log.debug("Searching for users with filter: \'" + filter + "\'" + " from base dn: " + _userBaseDn);
260 
261         try
262         {
263 
264             NamingEnumeration<SearchResult> results = _rootContext.search(_userBaseDn, filter, ctls);
265 
266             Log.debug("Found user?: " + results.hasMoreElements());
267 
268             if (results.hasMoreElements())
269             {
270                 SearchResult result = results.nextElement();
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             }
290             else
291             {
292                 throw new LoginException("User not found.");
293             }
294         }
295         catch (NamingException e)
296         {
297             throw new LoginException("Root context binding failure.");
298         }
299 
300         Log.debug("user cred is: " + ldapCredential);
301 
302         return ldapCredential;
303     }
304 
305     /**
306      * attempts to get the users roles from the root context
307      * <p/>
308      * NOTE: this is not an user authenticated operation
309      *
310      * @param username
311      * @return
312      * @throws LoginException
313      */
314     private List getUserRoles(String username) throws LoginException
315     {
316         ArrayList roleList = new ArrayList();
317 
318         SearchControls ctls = new SearchControls();
319 
320         ctls.setDerefLinkFlag(true);
321         ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
322 
323         if (_roleBaseDn != null)
324         {
325             String userDn = _userRdnAttribute + "=" + username + "," + _userBaseDn;
326             String filter = "(&(objectClass=" + _roleObjectClass + ")(" + _roleMemberAttribute + "=" + userDn + "))";
327             //filter  = doRFC2254Encoding(filter)
328             try
329             {
330                 NamingEnumeration<SearchResult> results = _rootContext.search(_roleBaseDn, filter, ctls);
331 
332                 Log.debug("Found user roles?: " + results.hasMoreElements());
333 
334                 while (results.hasMoreElements())
335                 {
336                     SearchResult result = results.nextElement();
337 
338                     Attributes attributes = result.getAttributes();
339 
340                     if (attributes != null)
341                     {
342                         Attribute roleAttribute = attributes.get(_roleNameAttribute);
343 
344                         if (roleAttribute != null)
345                         {
346                             NamingEnumeration roles = roleAttribute.getAll();
347                             while (roles.hasMore())
348                             {
349                                 String roleName = (String) roles.next();
350                                 roleList.add(roleName);
351                             }
352                         }
353                     }
354                 }
355             }
356             catch (NamingException e)
357             {
358                 throw new LoginException("error obtaining roles for " + username);
359             }
360         }
361 
362         return roleList;
363     }
364 
365 
366     /**
367      * since ldap uses a context bind for valid authentication checking, we override login()
368      * <p/>
369      * if credentials are not available from the users context or if we are forcing the binding check
370      * then we try a binding authentication check, otherwise if we have the users encoded password then
371      * we can try authentication via that mechanic
372      *
373      * @return
374      * @throws LoginException
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             UserInfo userInfo = getUserInfo(webUserName);
398 
399             setCurrentUser(new JAASUserInfo(userInfo));
400 
401             // if the userInfo query didn't pick up a password then opt for bindingLogin
402             if (userInfo.getCredential() == null || _forceBindingLogin)
403             {
404                 return bindingLogin(webUserName, webCredential);
405             }
406             else
407             {             
408                 if (webCredential instanceof String)
409                 {
410                     return credentialLogin(Credential.getCredential((String) webCredential));
411                 }
412                 else
413                 {
414                     return credentialLogin(webCredential);
415                 }
416             }
417 
418         }
419         catch (UnsupportedCallbackException e)
420         {
421             throw new LoginException("Error obtaining callback information.");
422         }
423         catch (IOException e)
424         {
425             throw new LoginException("IO Error performing login.");
426         }
427         catch (Exception e)
428         {
429             throw new LoginException("Error obtaining user info.");
430         }
431     }
432 
433     /**
434      * password supplied authentication check
435      *
436      * @param webCredential
437      * @return
438      * @throws LoginException
439      */
440 
441     protected boolean credentialLogin(Object webCredential) throws LoginException
442     {
443         setAuthenticated(getCurrentUser().checkCredential(webCredential));
444         return isAuthenticated();
445     }
446 
447     /**
448      * binding authentication check
449      * This methode of authentication works only if the user branch of the DIT (ldap tree)
450      * has an ACI (acces controle instruction) that allow the access to any user or at least
451      * for the user that logs in.
452      * 
453      * @param username
454      * @param password
455      * @return
456      * @throws LoginException
457      */
458     
459     protected boolean bindingLogin(String username, Object password) throws LoginException
460     {
461         SearchControls ctls = new SearchControls();
462 
463         ctls.setCountLimit(1);
464 
465         ctls.setDerefLinkFlag(true);
466         ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
467 
468         String filter = "(&(objectClass=" + _userObjectClass + ")(" + _userIdAttribute + "=" + username + "))";
469 
470         Log.info("Searching for users with filter: \'" + filter + "\'" + " from base dn: " + _userBaseDn);
471 
472         try
473         {
474 
475             NamingEnumeration<SearchResult> results = _rootContext.search(_userBaseDn, filter, ctls);
476 
477             Log.info("Found user?: " + results.hasMoreElements());
478 
479             if (results.hasMoreElements())
480             {
481                 SearchResult result = results.nextElement();
482 
483                 String userDn = result.getNameInNamespace();
484 
485                 Log.info("Attempting Authenication: + " + userDn);
486 
487                 Hashtable environment = getEnvironment();
488                 environment.put(Context.SECURITY_PRINCIPAL, userDn);
489                 environment.put(Context.SECURITY_CREDENTIALS, password);
490 
491                 try
492                 {
493                     DirContext userContext = new InitialDirContext( environment );
494 
495                 }
496                 catch (AuthenticationException e)
497                 {
498                     Log.info("Authentication failed for: " + userDn);
499                     throw new LoginException();
500                 }
501             catch (NamingException ne)
502         {
503             throw new LoginException("Context binding failure.");
504         }
505                
506                 setAuthenticated(true);
507 
508                 return true;
509             }
510             else
511             {
512                 throw new LoginException("User not found.");
513             }
514         }
515         catch (NamingException e)
516         {
517             throw new LoginException("Context binding failure.");
518         }
519 
520     }
521 
522 
523     /**
524      * Init LoginModule.
525      * Called once by JAAS after new instance is created.
526      *
527      * @param subject
528      * @param callbackHandler
529      * @param sharedState
530      * @param options
531      */
532     public void initialize(Subject subject,
533                            CallbackHandler callbackHandler,
534                            Map sharedState,
535                            Map options)
536     {
537 
538         super.initialize(subject, callbackHandler, sharedState, options);
539 
540         _hostname = (String) options.get("hostname");
541         _port = Integer.parseInt((String) options.get("port"));
542         _contextFactory = (String) options.get("contextFactory");
543         _bindDn = (String) options.get("bindDn");
544         _bindPassword = (String) options.get("bindPassword");
545         _authenticationMethod = (String) options.get("authenticationMethod");
546 
547         _userBaseDn = (String) options.get("userBaseDn");
548 
549         _roleBaseDn = (String) options.get("roleBaseDn");
550 
551         if (options.containsKey("forceBindingLogin"))
552         {
553             _forceBindingLogin = Boolean.parseBoolean((String) options.get("forceBindingLogin"));
554         }
555 
556         if (options.containsKey("userObjectClass"))
557         {
558             _userObjectClass = (String) options.get("userObjectClass");
559         }
560 
561         if (options.containsKey("userRdnAttribute"))
562         {
563             _userRdnAttribute = (String) options.get("userRdnAttribute");
564         }
565 
566         if (options.containsKey("userIdAttribute"))
567         {
568             _userIdAttribute = (String) options.get("userIdAttribute");
569         }
570 
571         if (options.containsKey("userPasswordAttribute"))
572         {
573             _userPasswordAttribute = (String) options.get("userPasswordAttribute");
574         }
575 
576         if (options.containsKey("roleObjectClass"))
577         {
578             _roleObjectClass = (String) options.get("roleObjectClass");
579         }
580         if (options.containsKey("roleMemberAttribute"))
581         {
582             _roleMemberAttribute = (String) options.get("roleMemberAttribute");
583         }
584         if (options.containsKey("roleNameAttribute"))
585         {
586             _roleNameAttribute = (String) options.get("roleNameAttribute");
587         }
588 
589         try
590         {
591             _rootContext = new InitialDirContext(getEnvironment());
592         }
593         catch (NamingException ex)
594         {
595             throw new IllegalStateException("Unable to establish root context", ex);
596         }
597 
598     }
599 
600     /**
601      * get the context for connection
602      *
603      * @return
604      */
605     public Hashtable<Object, Object> 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 ( encryptedPassword.toUpperCase().startsWith( "MD5:" ) )
644         {
645             String epwd = encryptedPassword.substring( "MD5:".length(), encryptedPassword.length() );
646             return "{MD5}" + epwd;
647         }
648         else if ( encryptedPassword.toUpperCase().startsWith( "CRYPT:" ) )
649         {
650             String epwd = encryptedPassword.substring( "CRYPT:".length(), encryptedPassword.length() );
651             return "{CRYPT}" + epwd;
652         }
653         else
654         {
655             return encryptedPassword;
656         }
657     }
658 
659     public static String convertCredentialLdapToJetty( String encryptedPassword )
660     {
661         if ( encryptedPassword.toUpperCase().startsWith( "{MD5}" ) )
662         {
663             String epwd = encryptedPassword.substring( "{MD5}".length(), encryptedPassword.length() );
664             return "MD5:" + epwd;
665         }
666         else if ( encryptedPassword.toUpperCase().startsWith( "{CRYPT}" ) )
667         {
668             String epwd = encryptedPassword.substring( "{CRYPT}".length(), encryptedPassword.length() );
669             return "CRYPT:" + epwd;
670         }
671         else
672         {
673             return encryptedPassword;
674         }
675     }
676 
677     public static byte[] digestMD5(String pwd) throws LoginException
678     {
679         MessageDigest md;
680 
681         byte[] barray;
682         try
683         {
684             md = MessageDigest.getInstance("MD5");
685             barray = pwd.getBytes("ISO-8859-1");//todo try w/ UTF8
686         }
687         catch (UnsupportedEncodingException e)
688         {
689             throw new LoginException();
690         }
691         catch (NoSuchAlgorithmException e1)
692         {
693             throw new LoginException();
694         }
695         for (int i = 0; i < barray.length; i++)
696         {
697             md.update(barray[i]);
698         }
699         String mdString = md.toString();
700 
701         return md.digest();
702 
703     }
704 }