External password authentication

From OpenCms Wiki
Revision as of 14:28, 9 March 2010 by 147.251.3.64 (Talk)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

OpenCms stores user accounts including their passwords in its database. However, sometimes you need to use password from some external source, like LDAP server or Kerberos.

I have not found any documentation how to do it, so here are my findings. This is for OpenCMS 7.5.2.

The LDAP modules listed on this wiki are not usable. None of them provides source code. The Langhua one is for OpenCMS 7.0 and is available only in binary form.

However, I have investigated how the login process goes. A user logs in on the /system/login/index.html JSP page, which has the following source:

<%@ page import="org.opencms.workplace.*" %>
<% CmsLogin wp = new CmsLogin(pageContext, request, response); %>
<%= wp.displayDialog() %>

Then the chain of calls is (a stacktrace in reverse order):

       at org.opencms.db.CmsDriverManager.loginUser(CmsDriverManager.java:4762)
       at org.opencms.db.CmsSecurityManager.loginUser(CmsSecurityManager.java:2883)
       at org.opencms.file.CmsObject.loginUser(CmsObject.java:2263)
       at org.opencms.jsp.CmsJspLoginBean.login(CmsJspLoginBean.java:189)
       at org.opencms.jsp.CmsJspLoginBean.login(CmsJspLoginBean.java:169)
       at org.opencms.workplace.CmsLogin.displayDialog(CmsLogin.java:293)
       at org.apache.jsp.WEB_002dINF.jsp.online.system.login.index_html_jsp._jspService(index_html_jsp.java:59)

Unfortunately, none of this places provides any hook or configuration. The CmsDriverManager and CmsSecurityManager classes are even final, so you cannot extend them.

However, the CmsDriverManager.loginUser() method calls method readUser() on an implementation of CmsUserDriver, and the implementation is configured in the opencms.properties file dependent on database, so in the default MySQL case it is:

# from opencms.properties
db.user.driver=org.opencms.db.mysql.CmsUserDriver

Here is the only place (that I have found) where you can put your code.

The solution

The org.opencms.db.mysql.CmsUserDriver extends the class org.opencms.db.generic.CmsUserDriver and adds nothing to it. So my solution is to create a new implementation of CmsUserDriver which extends org.opencms.db.generic.CmsUserDriver and overrides its readUser() method.

Here is the code :

package cz.cesnet.meta.opencms;
 
import org.apache.commons.logging.Log;
import org.opencms.configuration.CmsConfigurationManager;
import org.opencms.db.CmsDbContext;
import org.opencms.db.CmsDbEntryNotFoundException;
import org.opencms.db.CmsDriverManager;
import org.opencms.db.Messages;
import org.opencms.db.generic.CmsUserDriver;
import org.opencms.file.CmsDataAccessException;
import org.opencms.file.CmsUser;
import org.opencms.i18n.CmsMessageContainer;
import org.opencms.main.CmsLog;
import org.opencms.security.CmsPasswordEncryptionException;
public class ExternalAuthenticationCmsUserDriver extends CmsUserDriver {
 
    private static final Log LOG = CmsLog.getLog(ExternalAuthenticationCmsUserDriver.class);
 
    UserPasswordCheck userPasswordCheck;
 
    @SuppressWarnings({"unchecked"})
    @Override
    public void init(CmsDbContext dbc, CmsConfigurationManager configurationManager, List successiveDrivers, CmsDriverManager driverManager) {
        super.init(dbc, configurationManager, successiveDrivers, driverManager);
        Map config = configurationManager.getConfiguration();
 
        String classname = (String) config.get("db.user.passwordcheck");
        if (classname == null) classname = NoPasswordCheck.class.getName();
        try {
            Class aClass = Class.forName(classname);
            userPasswordCheck = (UserPasswordCheck) aClass.newInstance();
            userPasswordCheck.init(config);
        } catch (Exception e) {
            LOG.error("cannot load " + classname, e);
        }
        LOG.info("ExternalAuthenticationCmsUserDriver initialized");
    }
 
    @Override
    public CmsUser readUser(CmsDbContext dbc, String userFqn, String password, String remoteAddress) 
                                       throws CmsDataAccessException, CmsPasswordEncryptionException {
        if (LOG.isDebugEnabled()) LOG.debug("readUser(" + userFqn + ")");
        CmsUser user = super.readUser(dbc, userFqn);
        if (user != null) {
            try {
                userPasswordCheck.check(user, password);
            } catch (Exception ex) {
                CmsMessageContainer message = Messages.get().container(Messages.ERR_UNKNOWN_USER_1,userFqn);
                throw new CmsDbEntryNotFoundException(message);
            }
        }
        return user;
    }
}

The opencms.properties file has properties setting the new user driver, the implementation of my UserPasswordCheck interface and some properties for config information for the implementation:

db.user.driver=cz.cesnet.meta.opencms.ExternalAuthenticationCmsUserDriver
db.user.passwordcheck=cz.cesnet.meta.opencms.LdapPasswordCheck
db.user.ldap.url=ldap://some.ldap.machine/dc=foo,dc=bar

The UserPasswordCheck interface is here:

package cz.cesnet.meta.opencms;
 
import java.util.Map;
 
/**
 * Interface for external password check.
 *
 * @author Martin Kuba makub@ics.muni.cz
 */
public interface UserPasswordCheck {
 
    /**
     * Initializes from values in opencms.properties.
     * @param config map of values from opencms.properties
     */
    void init(Map<String,Object> config);
 
    /**
     * Checks password. Throws an exception if unsuccessful.
     *
     * @param user     cms user object
     * @param password password
     * @throws Exception when unsucessful
     */
    void check(CmsUser user, String password) throws Exception;
 
}

You can implement anything inside the check() method, like authentication against a LDAP server, or calling a Kerberos KDC center etc.

Personal tools
Namespaces
Variants
Actions
Navigation
Toolbox