View Javadoc

1   // ========================================================================
2   // Copyright (c) 2007-2009 Mort Bay Consulting Pty. Ltd.
3   // ------------------------------------------------------------------------
4   // All rights reserved. This program and the accompanying materials
5   // are made available under the terms of the Eclipse Public License v1.0
6   // and Apache License v2.0 which accompanies this distribution.
7   // The Eclipse Public License is available at 
8   // http://www.eclipse.org/legal/epl-v10.html
9   // The Apache License v2.0 is available at
10  // http://www.opensource.org/licenses/apache2.0.php
11  // You may elect to redistribute this code under either of these licenses. 
12  // ========================================================================
13  package org.eclipse.jetty.plus.jaas.spi;
14  
15  import java.io.IOException;
16  import java.util.ArrayList;
17  import java.util.Hashtable;
18  import java.util.List;
19  import java.util.Map;
20  import java.util.Properties;
21  
22  import javax.naming.Context;
23  import javax.naming.NamingEnumeration;
24  import javax.naming.NamingException;
25  import javax.naming.directory.Attribute;
26  import javax.naming.directory.Attributes;
27  import javax.naming.directory.DirContext;
28  import javax.naming.directory.InitialDirContext;
29  import javax.naming.directory.SearchControls;
30  import javax.naming.directory.SearchResult;
31  import javax.security.auth.Subject;
32  import javax.security.auth.callback.Callback;
33  import javax.security.auth.callback.CallbackHandler;
34  import javax.security.auth.callback.NameCallback;
35  import javax.security.auth.callback.UnsupportedCallbackException;
36  import javax.security.auth.login.LoginException;
37  
38  import org.eclipse.jetty.http.security.Credential;
39  import org.eclipse.jetty.plus.jaas.callback.ObjectCallback;
40  import org.eclipse.jetty.util.log.Log;
41  
42  /**
43   * A LdapLoginModule for use with JAAS setups
44   * <p/>
45   * The jvm should be started with the following parameter:
46   * <br><br>
47   * <code>
48   * -Djava.security.auth.login.config=etc/ldap-loginModule.conf
49   * </code>
50   * <br><br>
51   * and an example of the ldap-loginModule.conf would be:
52   * <br><br>
53   * <pre>
54   * ldaploginmodule {
55   *    org.eclipse.jetty.server.server.plus.jaas.spi.LdapLoginModule required
56   *    debug="true"
57   *    contextFactory="com.sun.jndi.ldap.LdapCtxFactory"
58   *    hostname="ldap.example.com"
59   *    port="389"
60   *    bindDn="cn=Directory Manager"
61   *    bindPassword="directory"
62   *    authenticationMethod="simple"
63   *    forceBindingLogin="false"
64   *    userBaseDn="ou=people,dc=alcatel"
65   *    userRdnAttribute="uid"
66   *    userIdAttribute="uid"
67   *    userPasswordAttribute="userPassword"
68   *    userObjectClass="inetOrgPerson"
69   *    roleBaseDn="ou=groups,dc=example,dc=com"
70   *    roleNameAttribute="cn"
71   *    roleMemberAttribute="uniqueMember"
72   *    roleObjectClass="groupOfUniqueNames";
73   *    };
74   *  </pre>
75   *
76   * 
77   * 
78   * 
79   */
80  public class LdapLoginModule extends AbstractLoginModule
81  {
82      /**
83       * hostname of the ldap server
84       */
85      private String _hostname;
86  
87      /**
88       * port of the ldap server
89       */
90      private int _port;
91  
92      /**
93       * Context.SECURITY_AUTHENTICATION
94       */
95      private String _authenticationMethod;
96  
97      /**
98       * Context.INITIAL_CONTEXT_FACTORY
99       */
100     private String _contextFactory;
101 
102     /**
103      * root DN used to connect to
104      */
105     private String _bindDn;
106 
107     /**
108      * password used to connect to the root ldap context
109      */
110     private String _bindPassword;
111 
112     /**
113      * object class of a user
114      */
115     private String _userObjectClass = "inetOrgPerson";
116 
117     /**
118      * attribute that the principal is located
119      */
120     private String _userRdnAttribute = "uid";
121 
122     /**
123      * attribute that the principal is located
124      */
125     private String _userIdAttribute = "cn";
126 
127     /**
128      * name of the attribute that a users password is stored under
129      * <p/>
130      * NOTE: not always accessible, see force binding login
131      */
132     private String _userPasswordAttribute = "userPassword";
133 
134     /**
135      * base DN where users are to be searched from
136      */
137     private String _userBaseDn;
138 
139     /**
140      * base DN where role membership is to be searched from
141      */
142     private String _roleBaseDn;
143 
144     /**
145      * object class of roles
146      */
147     private String _roleObjectClass = "groupOfUniqueNames";
148 
149     /**
150      * name of the attribute that a username would be under a role class
151      */
152     private String _roleMemberAttribute = "uniqueMember";
153 
154     /**
155      * the name of the attribute that a role would be stored under
156      */
157     private String _roleNameAttribute = "roleName";
158 
159     private boolean _debug;
160 
161     /**
162      * if the getUserInfo can pull a password off of the user then
163      * password comparison is an option for authn, to force binding
164      * login checks, set this to true
165      */
166     private boolean _forceBindingLogin = false;
167 
168     private DirContext _rootContext;
169 
170     /**
171      * get the available information about the user
172      * <p/>
173      * for this LoginModule, the credential can be null which will result in a
174      * binding ldap authentication scenario
175      * <p/>
176      * roles are also an optional concept if required
177      *
178      * @param username
179      * @return
180      * @throws Exception
181      */
182     public UserInfo getUserInfo(String username) throws Exception
183     {
184         String pwdCredential = getUserCredentials(username);
185 
186         if (pwdCredential == null)
187         {
188             return null;
189         }
190 
191         pwdCredential = convertCredentialLdapToJetty(pwdCredential);
192 
193         //String md5Credential = Credential.MD5.digest("foo");
194         //byte[] ba = digestMD5("foo");
195         //System.out.println(md5Credential + "  " + ba );
196         Credential credential = Credential.getCredential(pwdCredential);
197         List roles = getUserRoles(_rootContext, username);
198 
199         return new UserInfo(username, credential, roles);
200     }
201 
202     protected String doRFC2254Encoding(String inputString)
203     {
204         StringBuffer buf = new StringBuffer(inputString.length());
205         for (int i = 0; i < inputString.length(); i++)
206         {
207             char c = inputString.charAt(i);
208             switch (c)
209             {
210                 case '\\':
211                     buf.append("\\5c");
212                     break;
213                 case '*':
214                     buf.append("\\2a");
215                     break;
216                 case '(':
217                     buf.append("\\28");
218                     break;
219                 case ')':
220                     buf.append("\\29");
221                     break;
222                 case '\0':
223                     buf.append("\\00");
224                     break;
225                 default:
226                     buf.append(c);
227                     break;
228             }
229         }
230         return buf.toString();
231     }
232 
233     /**
234      * attempts to get the users credentials from the users context
235      * <p/>
236      * NOTE: this is not an user authenticated operation
237      *
238      * @param username
239      * @return
240      * @throws LoginException
241      */
242     private String getUserCredentials(String username) throws LoginException
243     {
244         String ldapCredential = null;
245 
246         SearchControls ctls = new SearchControls();
247         ctls.setCountLimit(1);
248         ctls.setDerefLinkFlag(true);
249         ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
250 
251         String filter = "(&(objectClass={0})({1}={2}))";
252 
253         Log.debug("Searching for users with filter: \'" + filter + "\'" + " from base dn: " + _userBaseDn);
254 
255         try
256         {
257             Object[] filterArguments = {_userObjectClass, _userIdAttribute, username};
258             NamingEnumeration results = _rootContext.search(_userBaseDn, filter, filterArguments, ctls);
259 
260             Log.debug("Found user?: " + results.hasMoreElements());
261 
262             if (!results.hasMoreElements())
263             {
264                 throw new LoginException("User not found.");
265             }
266 
267             SearchResult result = findUser(username);
268 
269             Attributes attributes = result.getAttributes();
270 
271             Attribute attribute = attributes.get(_userPasswordAttribute);
272             if (attribute != null)
273             {
274                 try
275                 {
276                     byte[] value = (byte[]) attribute.get();
277 
278                     ldapCredential = new String(value);
279                 }
280                 catch (NamingException e)
281                 {
282                     Log.debug("no password available under attribute: " + _userPasswordAttribute);
283                 }
284             }
285         }
286         catch (NamingException e)
287         {
288             throw new LoginException("Root context binding failure.");
289         }
290 
291         Log.debug("user cred is: " + ldapCredential);
292 
293         return ldapCredential;
294     }
295 
296     /**
297      * attempts to get the users roles from the root context
298      * <p/>
299      * NOTE: this is not an user authenticated operation
300      *
301      * @param dirContext
302      * @param username
303      * @return
304      * @throws LoginException
305      */
306     private List getUserRoles(DirContext dirContext, String username) throws LoginException, NamingException
307     {
308         String userDn = _userRdnAttribute + "=" + username + "," + _userBaseDn;
309 
310         return getUserRolesByDn(dirContext, userDn);
311     }
312 
313     private List getUserRolesByDn(DirContext dirContext, String userDn) throws LoginException, NamingException
314     {
315         ArrayList roleList = new ArrayList();
316 
317         if (dirContext == null || _roleBaseDn == null || _roleMemberAttribute == null || _roleObjectClass == null)
318         {
319             return roleList;
320         }
321 
322         SearchControls ctls = new SearchControls();
323         ctls.setDerefLinkFlag(true);
324         ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
325 
326         String filter = "(&(objectClass={0})({1}={2}))";
327         Object[] filterArguments = {_roleObjectClass, _roleMemberAttribute, userDn};
328         NamingEnumeration results = dirContext.search(_roleBaseDn, filter, filterArguments, ctls);
329 
330         Log.debug("Found user roles?: " + results.hasMoreElements());
331 
332         while (results.hasMoreElements())
333         {
334             SearchResult result = (SearchResult) results.nextElement();
335 
336             Attributes attributes = result.getAttributes();
337 
338             if (attributes == null)
339             {
340                 continue;
341             }
342 
343             Attribute roleAttribute = attributes.get(_roleNameAttribute);
344 
345             if (roleAttribute == null)
346             {
347                 continue;
348             }
349 
350             NamingEnumeration roles = roleAttribute.getAll();
351             while (roles.hasMore())
352             {
353                 roleList.add(roles.next());
354             }
355         }
356 
357         return roleList;
358     }
359 
360 
361     /**
362      * since ldap uses a context bind for valid authentication checking, we override login()
363      * <p/>
364      * if credentials are not available from the users context or if we are forcing the binding check
365      * then we try a binding authentication check, otherwise if we have the users encoded password then
366      * we can try authentication via that mechanic
367      *
368      * @return
369      * @throws LoginException
370      */
371     public boolean login() throws LoginException
372     {
373         try
374         {
375             if (getCallbackHandler() == null)
376             {
377                 throw new LoginException("No callback handler");
378             }
379 
380             Callback[] callbacks = configureCallbacks();
381             getCallbackHandler().handle(callbacks);
382 
383             String webUserName = ((NameCallback) callbacks[0]).getName();
384             Object webCredential = ((ObjectCallback) callbacks[1]).getObject();
385 
386             if (webUserName == null || webCredential == null)
387             {
388                 setAuthenticated(false);
389                 return isAuthenticated();
390             }
391 
392             if (_forceBindingLogin)
393             {
394                 return bindingLogin(webUserName, webCredential);
395             }
396 
397             // This sets read and the credential
398             UserInfo userInfo = getUserInfo(webUserName);
399 
400             if (userInfo == null)
401             {
402                 setAuthenticated(false);
403                 return false;
404             }
405 
406             setCurrentUser(new JAASUserInfo(userInfo));
407 
408             if (webCredential instanceof String)
409             {
410                 return credentialLogin(Credential.getCredential((String) webCredential));
411             }
412 
413             return credentialLogin(webCredential);
414         }
415         catch (UnsupportedCallbackException e)
416         {
417             throw new LoginException("Error obtaining callback information.");
418         }
419         catch (IOException e)
420         {
421             if (_debug)
422             {
423                 e.printStackTrace();
424             }
425             throw new LoginException("IO Error performing login.");
426         }
427         catch (Exception e)
428         {
429             if (_debug)
430             {
431                 e.printStackTrace();
432             }
433             throw new LoginException("Error obtaining user info.");
434         }
435     }
436 
437     /**
438      * password supplied authentication check
439      *
440      * @param webCredential
441      * @return
442      * @throws LoginException
443      */
444     protected boolean credentialLogin(Object webCredential) throws LoginException
445     {
446         setAuthenticated(getCurrentUser().checkCredential(webCredential));
447         return isAuthenticated();
448     }
449 
450     /**
451      * binding authentication check
452      * This methode of authentication works only if the user branch of the DIT (ldap tree)
453      * has an ACI (acces control instruction) that allow the access to any user or at least
454      * for the user that logs in.
455      *
456      * @param username
457      * @param password
458      * @return
459      * @throws LoginException
460      */
461     public boolean bindingLogin(String username, Object password) throws LoginException, NamingException
462     {
463         SearchResult searchResult = findUser(username);
464 
465         String userDn = searchResult.getNameInNamespace();
466 
467         Log.info("Attempting authentication: " + userDn);
468 
469         Hashtable environment = getEnvironment();
470         environment.put(Context.SECURITY_PRINCIPAL, userDn);
471         environment.put(Context.SECURITY_CREDENTIALS, password);
472 
473         DirContext dirContext = new InitialDirContext(environment);
474         List roles = getUserRolesByDn(dirContext, userDn);
475 
476         UserInfo userInfo = new UserInfo(username, null, roles);
477 
478         setCurrentUser(new JAASUserInfo(userInfo));
479 
480         setAuthenticated(true);
481 
482         return true;
483     }
484 
485     private SearchResult findUser(String username) throws NamingException, LoginException
486     {
487         SearchControls ctls = new SearchControls();
488         ctls.setCountLimit(1);
489         ctls.setDerefLinkFlag(true);
490         ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
491 
492         String filter = "(&(objectClass={0})({1}={2}))";
493 
494         Log.info("Searching for users with filter: \'" + filter + "\'" + " from base dn: " + _userBaseDn);
495 
496         Object[] filterArguments = new Object[]{
497             _userObjectClass,
498             _userIdAttribute,
499             username
500         };
501         NamingEnumeration results = _rootContext.search(_userBaseDn, filter, filterArguments, ctls);
502 
503         Log.info("Found user?: " + results.hasMoreElements());
504 
505         if (!results.hasMoreElements())
506         {
507             throw new LoginException("User not found.");
508         }
509 
510         return (SearchResult) results.nextElement();
511     }
512 
513 
514     /**
515      * Init LoginModule.
516      * Called once by JAAS after new instance is created.
517      *
518      * @param subject
519      * @param callbackHandler
520      * @param sharedState
521      * @param options
522      */
523     public void initialize(Subject subject,
524                            CallbackHandler callbackHandler,
525                            Map sharedState,
526                            Map options)
527     {
528 
529         super.initialize(subject, callbackHandler, sharedState, options);
530 
531         _hostname = (String) options.get("hostname");
532         _port = Integer.parseInt((String) options.get("port"));
533         _contextFactory = (String) options.get("contextFactory");
534         _bindDn = (String) options.get("bindDn");
535         _bindPassword = (String) options.get("bindPassword");
536         _authenticationMethod = (String) options.get("authenticationMethod");
537 
538         _userBaseDn = (String) options.get("userBaseDn");
539 
540         _roleBaseDn = (String) options.get("roleBaseDn");
541 
542         if (options.containsKey("forceBindingLogin"))
543         {
544             _forceBindingLogin = Boolean.parseBoolean((String) options.get("forceBindingLogin"));
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.parseBoolean(String.valueOf(getOption(options, "debug", Boolean.toString(_debug))));
555 
556         try
557         {
558             _rootContext = new InitialDirContext(getEnvironment());
559         }
560         catch (NamingException ex)
561         {
562             throw new IllegalStateException("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 		try 
582 		{
583 			_rootContext.close();
584 		} 
585 		catch (NamingException e) 
586 		{
587 			throw new LoginException( "error closing root context: " + e.getMessage() );
588 		}
589 		
590 		return super.abort();
591 	}
592 
593 	private String getOption(Map options, String key, String defaultValue)
594     {
595         Object value = options.get(key);
596 
597         if (value == null)
598         {
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<Object, Object> getEnvironment()
611     {
612         Properties env = new Properties();
613 
614         env.put(Context.INITIAL_CONTEXT_FACTORY, _contextFactory);
615 
616         if (_hostname != null)
617         {
618             if (_port != 0)
619             {
620                 env.put(Context.PROVIDER_URL, "ldap://" + _hostname + ":" + _port + "/");
621             }
622             else
623             {
624                 env.put(Context.PROVIDER_URL, "ldap://" + _hostname + "/");
625             }
626         }
627 
628         if (_authenticationMethod != null)
629         {
630             env.put(Context.SECURITY_AUTHENTICATION, _authenticationMethod);
631         }
632 
633         if (_bindDn != null)
634         {
635             env.put(Context.SECURITY_PRINCIPAL, _bindDn);
636         }
637 
638         if (_bindPassword != null)
639         {
640             env.put(Context.SECURITY_CREDENTIALS, _bindPassword);
641         }
642 
643         return env;
644     }
645 
646     public static String convertCredentialJettyToLdap(String encryptedPassword)
647     {
648         if ("MD5:".startsWith(encryptedPassword.toUpperCase()))
649         {
650             return "{MD5}" + encryptedPassword.substring("MD5:".length(), encryptedPassword.length());
651         }
652 
653         if ("CRYPT:".startsWith(encryptedPassword.toUpperCase()))
654         {
655             return "{CRYPT}" + encryptedPassword.substring("CRYPT:".length(), encryptedPassword.length());
656         }
657 
658         return encryptedPassword;
659     }
660 
661     public static String convertCredentialLdapToJetty(String encryptedPassword)
662     {
663         if (encryptedPassword == null)
664         {
665             return encryptedPassword;
666         }
667 
668         if ("{MD5}".startsWith(encryptedPassword.toUpperCase()))
669         {
670             return "MD5:" + encryptedPassword.substring("{MD5}".length(), encryptedPassword.length());
671         }
672 
673         if ("{CRYPT}".startsWith(encryptedPassword.toUpperCase()))
674         {
675             return "CRYPT:" + encryptedPassword.substring("{CRYPT}".length(), encryptedPassword.length());
676         }
677 
678         return encryptedPassword;
679     }
680 }