CASifying Sun Identity Manager

Introduction

The following guide explains how the University of California, Merced modified Sun Identity Manager (IDM) to implement CAS single sign on for the IDM user interface.  This document was originally written when we were running IDM 5.0.  I have since edited the code on this page so that the code should work on IDM 7.1.

CAS Resource Adapter

Compile and put edu.ucmerced.idm.resource.CASResourceAdapter in the class path of your deployed Identity Manager server. The code is attached at the bottom of this page.

Also download the CAS client and put casclient.jar in your deployed IDM's WEB-INF/lib (we're using CAS 2.0).

Configuring Identity Manager

Log in as Configurator.

Click Resources tab.

Click Configure Managed Resources button.

Add edu.ucmerced.idm.resource.CASResourceAdapter as a custom resource and click Save.

Go back to the Resources tab.

Select the CAS resource using the pull-down on the bottom of the page.

This will take you through the Create CAS Resource Wizard. There is nothing to configure for this resource. Just keep clicking Next and then Save on the last page.

Click the Configure tab.

Click Login on the left menu.

Click Manage Login Module Groups.

Set up a new group or edit your existing one. We create a new group with nothing but the CAS module

When editing the login group, select CAS Login Module from the pull-down menu. Select CAS from the pull-down menu that appears to the right.

Select the login success requirement. We use required, but you may want something different if you're using multiple modules for the group (refer to the Identity Manager documentation for the definitions of the different types).

Click Save.

If you created a new login module group, go back to Configure -> Login, and select the User Interface link. Remove the default module group and add the one you created. Click Save.

IMPORTANT: For every person you want to be able to allow to log in through CAS, you must assign the CAS resource to their IDM account. If CAS authentication succeeds but they don't have the CAS resource assigned, you will probably see a Java exception being thrown on the IDM server when CAS redirects to IDM. If you have an environment where you only want some of your CAS-enabled people to log into IDM and you want to gracefully handle the people who try to log in to IDM but aren't allowed, then you will have to invest some more time in determining a solution for this problem. For us, we simply assign everybody with an IDM account the CAS resource, and we do finer grained access control using IDM account attributes.

web.xml for Identity Manager

Add something that looks like this your WEB-INF/web.xml file:

<!-- CAS -->
  <filter>
    <filter-name>CAS Filter</filter-name>
    <filter-class>edu.yale.its.tp.cas.client.filter.CASFilter</filter-class>
    <init-param>
       <param-name>edu.yale.its.tp.cas.client.filter.loginUrl</param-name>
       <param-value>https://casserver.school.edu/cas/login</param-value>
    </init-param>
    <init-param>
       <param-name>edu.yale.its.tp.cas.client.filter.validateUrl</param-name>
       <param-value>https://casserver.school.edu/cas/serviceValidate</param-value>
    </init-param>
    <init-param>
       <param-name>edu.yale.its.tp.cas.client.filter.serverName</param-name>
       <param-value>idmserver.school.edu</param-value>
    </init-param>

    <!-- wrap request such that getRemoteUser() returns username -->
    <init-param>
      <param-name>edu.yale.its.tp.cas.client.filter.wrapRequest</param-name>
      <param-value>true</param-value>
    </init-param>
  </filter>

  <filter-mapping>
    <filter-name>CAS Filter</filter-name>
    <url-pattern>/user/*</url-pattern>
  </filter-mapping>

Logout

To handle logout properly, you need to both log out of CAS and to log out of the IDM session. To do this, edit user/userLogout.jsp where your IDM application is deployed and change the following:

if (wsSubj != null && SessionFactory.isUserInputRequired(0, wsSubj.getLoginApplication(), wsSubj.
    url = "user/login.jsp";
  else
    url = "user/staticUserLogout.jsp";

Change to:

url = "https://casserver.school.edu/cas/logout?destination=https://idmserver.school.edu/idm/user/";

This will first log you out of IDM then redirect to the CAS logout URL which will take care of the CAS logout. It will then redirect to https://idmserver.school.edu/idm/user/, which will ask the user to log back into IDM. You may want to redirect to a different URL, perhaps a page that notifies the user that he or she has been logged out. See user/staticUserLogout.jsp for an example, but if you use this, you will have to move it outside of the /user directory because it is CAS protected and the user won't be able to see it after they log out.

CASResourceAdapter.java

package edu.ucmerced.idm.adapter;

import java.util.*;
import org.w3c.dom.*;
import com.waveset.adapter.*;
import com.waveset.util.WavesetException;
import com.waveset.object.*;
import com.waveset.msgcat.Message;
import com.waveset.util.EncryptedData;
import com.waveset.logging.trace.WSTrace;
import com.waveset.session.Session;
import com.waveset.session.SessionFactory;
import com.waveset.repository.Repository;
import com.waveset.repository.ServerRepository;

import java.util.ArrayList;

//
// This is derived from a class named MyExampleLoginResourceAdapter that Sun
// sent us when we asked about SSO implementation.
//
// In the IDM distribution you can also take a look at
// REF/SkeletonStandardResourceAdapter.java.
//

/**
 * Allow pass-through authentication using CAS.
 * <p/>
 * This resource must be assigned to any user you wish to authenticate
 * through CAS.
 * <p/>
 * Known issues:
 * <p/>
 * - This is often seen when users assigned to this resource are
 *   checked out or checked in:</br>
 *   com.waveset.util.WavesetException: The adapter must support iterating accounts in order to reconcile accounts on resource 'CAS'.<br/>
 *   However, this appears to have no adverse affects on the checkin and
 *   checkout operations.
 *
 * @author Brian Koehmstedt (bkoehmstedt@ucmerced.edu)
 */
public class CASResourceAdapter extends ResourceAdapterBase
{
    public static final String RESOURCE_TYPE = "CAS";
    public static final String LOGIN_MODULE  = RESOURCE_TYPE + " Login Module";
    public static final String CONFIG_OBJECT = "CAS Configuration Object";
    private static ArrayList _refererList;

    // this is the request parameter that CAS sets when it authenticates a
    // user
    public static final String USER = "userid";

    // This is the IDM user attribute we search for when searching for a
    // user using the userid value CAS passes us.
    public static final String IDMATTR = "ldapId";

    public static final String REFERER = "Referer";

    public CASResourceAdapter(Resource res, ObjectCache cache)
    {
        super(res, cache);
    }

    public CASResourceAdapter()
    {
        super();
    }

    static final String prototypeXml =
    "<Resource name='CAS' class='edu.ucmerced.idm.adapter.CASResourceAdapter' typeString='" + RESOURCE_TYPE + "'>\n" +
    "  <Template>\n"+
    "    <AttrDef name='accountId' type='string' />\n" +
    "  </Template>\n" +
    "  <LoginConfigEntry name='" + Constants.WS_RESOURCE_LOGIN_MODULE +
    "'   type='" + RESOURCE_TYPE + "' displayName='" + LOGIN_MODULE + "'>\n" +
    "    <AuthnProperties>\n" +
    "      <AuthnProperty name='" + REFERER + "' formFieldType='text' dataSource='" + Constants.HTTP_HEADER_DATA_SOURCE+ "'/>\n" +
    "      <AuthnProperty name='" + USER + "' formFieldType='text' isId='true' dataSource='" + Constants.HTTP_REMOTE_USER_DATA_SOURCE + "'/>\n" +
    "    </AuthnProperties>\n" +
    "    <SupportedApplications>\n" +
    "      <SupportedApplication name='"+Constants.ADMINCONSOLE+"'/>\n" +
    "      <SupportedApplication name='"+Constants.SELFPROVISION+"'/>\n "+
    "    </SupportedApplications>\n" +
    "  </LoginConfigEntry>\n" +
    "</Resource>\n";

    // Create prototype of the resource
    public static Resource staticCreatePrototypeResource() throws WavesetException
    {
        Resource res = new Resource(prototypeXml);
        return res;
    }

    public Resource createPrototypeResource() throws WavesetException
    {
        return staticCreatePrototypeResource();
    }

    //////////////////////////////////////////////////////////////////////
    //
    // Account operations
    //
    //////////////////////////////////////////////////////////////////////

    public GenericObject getFeatures()
    {
      // currently no features supported by this resource adapter, except a shallow create/delete
      GenericObject genObj = new GenericObject();
      genObj.put(ResourceAdapter.Features.ACCOUNT_CREATE, ResourceAdapter.Features.ACCOUNT_CREATE);
      genObj.put(ResourceAdapter.Features.ACCOUNT_UPDATE, ResourceAdapter.Features.ACCOUNT_UPDATE);
      genObj.put(ResourceAdapter.Features.ACCOUNT_DELETE, ResourceAdapter.Features.ACCOUNT_DELETE);
      genObj.put(ResourceAdapter.Features.ACCOUNT_LOGIN, ResourceAdapter.Features.ACCOUNT_LOGIN);
      genObj.put(ResourceAdapter.Features.ACCOUNT_ITERATOR, ResourceAdapter.Features.ACCOUNT_ITERATOR);
      return genObj;
    }

    public WSUser getUser(WSUser user) throws WavesetException
    {

        final String method = "getUser";
        if (_trace.level1(this,method))
        {
            _trace.entry(WSTrace.LEVEL1, this, method);
        }

        WSUser newUser = null;
        String identity = null;
        ResourceInfo resInfo = user.getResourceInfo(_resource);

        // if there exist a resource info for this resource account and LH
        // believes the account exists, then return a WSUser with the minimal
        // settings. Otherwise, return null to indicate to LH one needs to be
        // created.
        if (resInfo != null && resInfo.isAccountCreated())
        {
            identity = resInfo.getAccountID();
            if ((identity == null) || (identity.length() <= 0))
            {
                throw new WavesetException("No identity defined for User '" + user.getName() +
                       "' on Resource '" + _resource.getName() + "'.");
            }
            newUser = new WSUser();
            newUser.setName(identity);
            newUser.setAccountId(identity);
        }
        if (_trace.level1(this,method)) {
        _trace.exit(WSTrace.LEVEL1, this, method);
        }
        return newUser;
    }

    public WavesetResult checkCreateAccount(WSUser user) throws WavesetException
    {
        final String method = "checkCreateAccount";
        if (_trace.level1(this,method))
        {
            _trace.entry(_trace.LEVEL1, this, method, user.getName());
        }

        WavesetResult result = new WavesetResult();

        // add-code-here to check to make sure that the resource can be
        // contacted, that the privileged account name and password being
        // used to create the account is in order, and that account attribute
        // values comply with any resource specific policies (no duplicate ids
        // for example)

        // Common exceptions to use:
        // Invalid arguments use:
        // throw new com.waveset.util.InvalidArgument("Not a valid " + RA_HOST);
        // Invalid principal account or password use:
        // throw new WavesetException("Cound not authenticate to the server.");
        // Resource unavailable use:
        // throw new WavesetException("Could not connect to the server.");
        if (_trace.level1(this,method))
        {
            _trace.exit(_trace.LEVEL1, this, method);
        }
        return result;
    }

    protected void realCreate(WSUser user, WavesetResult result) throws WavesetException
    {
        // Create a new user account on the resource
        // Recieves as input a User object

        final String method = "realCreate";
        if (_trace.level1(this,method))
        {
        _trace.entry(WSTrace.LEVEL1, this, method);
        }
        // use the user object passed in and find the user's accountId for
        // this resource.
        // getIdentity returns the account name for this user.
        String dn = getIdentity(user);
        ResourceInfo resInfo = user.getResourceInfo(_resource);

        // As a way to indicate that the user password was successfully
        // set on the resource, the password field in the resource info on
        // the user object is set to null.
        //
        // add-code-here to check that the password was set for the user
        // account and then set the resource info password to null.
        if (resInfo != null)
        {
            resInfo.setAccountCreated(true);
            // indicate that the password was set
            resInfo.setPassword((EncryptedData)null);
        }

        if (_trace.level3(this,method))
        {
            String msg = "Added user '" + dn + "'";
            _trace.info(_trace.LEVEL3, this, method, msg);
        }

        if (_trace.level1(this,method))
        {
            _trace.exit(_trace.LEVEL1, this, method);
        }
    }

    public WavesetResult checkUpdateAccount(WSUser user) throws WavesetException
    {
        // Check to see if the account can be updated. See checkCreate for list of
        // things that can be checked.
        // The checkUpdateAccount will take as input a User object.
        // If the account does not exist, throw exception.
        // Common exceptions to use:
        // Invalid arguments use:
        //    throw new com.waveset.util.InvalidArgument("Not a valid " + RA_HOST);
        // Invalid principal account or password use:
        //    throw new WavesetException("Cound not authenticate to the server.");
        // Resource unavailable use:
        //    throw new WavesetException("Could not connect to the server.");
        final String method = "checkUpdateAccount";
        if (_trace.level1(this,method))
        {
            _trace.entry(_trace.LEVEL1, this, method, user.getName());
        }
        WavesetResult result = new WavesetResult();
        if (_trace.level1(this,method))
        {
            _trace.exit(_trace.LEVEL1, this, method);
        }
        return result;
    }


    protected void realUpdate(WSUser user, WavesetResult result) throws WavesetException
    {
        // Update account.
        //
        // Receives as input a User object.
        //
        // updateAccount method needs to modify the individual account attributes
        // and not replace the entire record. The user password will only be
        // present if it is needing to be changed. The password if it does need to
        // be changed will not be found in the user.password variable passed in,
        // it will instead be found on the resource info object password
        //
        final String method = "realUpdate";

        if (_trace.level1(this,method))
        {
            _trace.entry(_trace.LEVEL1, this, method, user.getName());
        }

        String dn = getIdentity(user);
        // EncryptedData pw = resInfo.getPassword();
        if (_trace.level3(this,method))
        {
            _trace.info(_trace.LEVEL2, this, method, "Updating user '"+user+"'");
        }
        // add-code-here to update user record
        // As a way to indicate that the user password was successfully
        // set on the resource, the password field in the resource info on
        // the user object is set to null.
        //
        // add-code-here to check that the password was set for the user
        // account and then set the resource info password to null.
        ResourceInfo resInfo = user.getResourceInfo(_resource);
        if (resInfo != null)
        {
            // Indicate that the password was set
            resInfo.setPassword((EncryptedData)null);
        }
    }


    public WavesetResult checkDeleteAccount(WSUser user) throws WavesetException
    {
        // Check to see if the account can be deleted. See checkCreate for list of
        // things that can be checked (with the exception of the account attribute
        // compliance).
        //
        // Receives a User object as input. The user's identity on this resource
        // will be used to identify the account to be deleted.
        //
        // checkDeleteAccount should not fail if account does not exist
        // Common exceptions to use:
        // Invalid arguments use:
        //    throw new com.waveset.util.InvalidArgument("Not a valid " + RA_HOST);
        // Invalid principal account or password use:
        //    throw new WavesetException("Cound not authenticate to the server.");
        // Resource unavailable use:
        //    throw new WavesetException("Could not connect to the server.");
        final String method = "checkDeleteAccount";

        if (_trace.level1(this,method))
        {
            _trace.entry(_trace.LEVEL1, this, method, user.getName());
        }

        WavesetResult result = new WavesetResult();
        String identity = getIdentity(user);
        if (_trace.level1(this,method))
        {
            _trace.exit(_trace.LEVEL1, this, method);
        }
        return result;
    }

    protected void realDelete(WSUser user, WavesetResult result) throws WavesetException
    {
        // Delete account
        //
        // Receives as input a user object.
        // Result should be an error if account cannot be deleted
        //
        // result.addError(msg) can be used to add detailed info on why
        // the account cannot be deleted.
        final String method = "realDelete";

        if (_trace.level1(this,method))
        {
            _trace.entry(_trace.LEVEL1, this, method, user.getName());
        }
        String dn = getIdentity(user);
        // add-code-here to delete user account
    }

    public WSAttributes getAccountAttributes(String accountIdentity) throws WavesetException
    {
        final String method = "getAccountAttributes";

        if (_trace.level1(this,method))
        {
            _trace.entry(_trace.LEVEL1, this, method);
        }
        WavesetResult result = new WavesetResult();
        WSAttributes wsAttrs = null;

        if (_trace.level1(this,method))
        {
            _trace.exit(_trace.LEVEL1, this, method);
        }

        return wsAttrs;
    }

    public AccountIterator getAccountIterator() throws WavesetException
    {
        // Returns an iterator that can be used to iterate over all the
        // accounts on a resource.
        //
        // AccountIterator is used for auto-discovery methods to get bulk user
        // accounts from the resource.
        final String method = "getAccountIterator";
        if (_trace.level1(this,method))
            _trace.entry(_trace.LEVEL1, this, method);

        AccountIterator acctIter = null;

        if (_trace.level1(this,method))
        _trace.exit(_trace.LEVEL1, this, method);

        return acctIter;
    }

    public WavesetResult authenticate(HashMap loginInfo) throws WavesetException
    {
        // Authenticate method is used to verify a user account and password are
        // valid. If the user account name does not exist on the resource, the
        // password does not match, or multiple matches exist, then throw an
        // exception. The search does not stop when it finds the first match,
        // instead it will continue through the hole list.

        final String method = "authenticate";
        if (_trace.level1(this,method))
        {
            _trace.entry(WSTrace.LEVEL1, this, method);
        }

        WavesetResult result = new WavesetResult();

        String userId = (String)loginInfo.get(USER);
        if(_trace.level2(this,method))
        {
          _trace.info(_trace.LEVEL2, this, method, USER + " = " + userId);
          _trace.info(_trace.LEVEL2, this, method, "map: " +  loginInfo);
        }

        if (_trace.level2(this,method))
        {
            _trace.info(_trace.LEVEL2, this, method, "Obtained user '" + userId + "' from info: " + String.valueOf(loginInfo));
        }

        String accountId = null;
        if (userId != null)
        {
          HashMap conds = new HashMap();
          conds.put(IDMATTR, userId);
          List users = executeQuery(Type.USER, conds);
          if(users != null && users.size() == 1)
          {
            accountId = ((RepositoryResult.Row)users.get(0)).getName();
            if(_trace.level1(this,method))
              _trace.info(_trace.LEVEL1, this, method, "found user with accountId=" + accountId);
          }
          else
          {
            if(_trace.level1(this,method))
              _trace.info(_trace.LEVEL1, this, method, "couldn't find user with " + IDMATTR + "=" + userId);
          }
        }

        if(accountId == null)
        {
          throw new WavesetException("Authentication failed using CAS");
        }
        else
        {
          result.addResult(Constants.AUTHENTICATED_IDENTITY, accountId);
        }
        return result;
    }


    protected void startConnection() throws WavesetException
    {
    }


    protected void stopConnection() throws WavesetException
    {
    }


    private GenericObject loadConfigData() throws WavesetException
    {
        if (_cache == null)
        {
            throw new WavesetException("Error looking up Configuration Object");
        }
        Configuration params =
        (Configuration) _cache.getObject(Type.CONFIGURATION, CONFIG_OBJECT);

        if (params == null)
        {
            throw new WavesetException("Configuration named " + CONFIG_OBJECT
                     + " not found");
        }

        GenericObject configObject = (GenericObject) params.getExtension();
      return configObject;
    }

    // returns a list of RepositoryResult.Rows
    protected static List executeQuery(Type type, Map attributeMap)
      throws com.waveset.util.WavesetException
    {
      return(executeQuery(type, new WSAttributes(attributeMap)));
    }

    // returns a list of RepositoryResult.Rows
    protected static List executeQuery(Type type, WSAttributes wsattrs)
      throws WavesetException
    {
      LinkedList foundRows = new LinkedList();
      Repository repository = ServerRepository.getRepository(true);
      RepositoryResult result = repository.list(type, wsattrs);
      while(result != null && result.hasNext())
      {
        RepositoryResult.Row row = result.next();
        foundRows.add(row);
      }
      return(foundRows);
    }
}