View Javadoc

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