View Javadoc

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