View Javadoc

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