View Javadoc

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