Installing a Custom OpenSSO Identity Repository

David Holroyd

Head of Server-Side Development
Goodtechnology Ltd.

Table of Contents

Introduction
The Repository
Implementation Basics
Data Store Plugin Lifecycle
Declaring Capabilities
Implementing the Declared Operations
Implementing the Rest
Usage patterns
Adding the Repository Plugin into the OpenSSO Server
Create a new Realm
Adding the Data Store to the new Realm
A Little Test of the Data Store
Authenticating Against the Data Store
Creating the Authentication Chain

Introduction

These notes show how to implement a simple OpenSSO 'Data Store' on top of a relational database. We discuss the coding required and then show how to configure OpenSSO to use our plugin using its web-based administration console.

The Repository

The situation we want to address is that of needing to provide Web Single Sign-On and account management to users defined by an existing relational database. While it is possible to configure a very basic JDBC Authentication module in OpenSSO, this does not provide any identity management features other than authentication. To provide OpenSSO with 'full' access to the identity repository, we must implement a Data Store adapter, and configure OpenSSO to use this.

To simplify the implementation of this example, the user account information will be stored in an HSQLDB in-memory database. A real application would of course need to deal with the issue of how to obtain JDBC configuration (and would probably require that data persist between application restarts).

Implementation Basics

OpenSSO Data Store plugins must implement the class com.sun.identity.idm.IdRepo. This class has a number of abstract methods which must be implemented, and some other methods whose default implementation must be overriden in order to be able to provide a useful service.

Our IdRepo will be implemented in terms of a simplistic JDBC Data Access Object.

Data Store Plugin Lifecycle

In order for OpenSSO to be able to load our plugin, the IdRepo subclass must have a no-args constructor for OpenSSO to call.

When OpenSSO loads our plugin, it will call its initialize() method. We use this opportunity in our implementation to initialize the Data Access Object.

There is also a chance to perform any cleanup needed when OpenSSO is finished with our plugin, by overriding the shutdown() method.

The IdRepo class also requires that implementations provide addListener() and removeListener() methods. While it is not entirely clear what these methods are for, OpenSSO will not be able to use our plugin correctly if they raise exceptions, so we provide basic no-op implementations.

	public void initialize(Map configParams) {
	      super.initialize(configParams);
	      dao = new IdDAO();
	      try {
		      dao.init();
	      } catch (SQLException e) {
		      e.printStackTrace();
	      }
      }

      public void shutdown() {
	      super.shutdown();
      }

      public int addListener(SSOToken token, IdRepoListener listener)
	      throws IdRepoException, SSOException
      {
	      // TODO: no idea what this needs to do
	      return 0;
      }

      public void removeListener() {
	      // TODO: no idea what this needs to do
      }

Declaring Capabilities

Data Stores can have varying capabilities in terms of the kind of subjects they support and the operations that they will perform on those subjects. In order to tell OpenSSO what our plugin is capable of we need to override the methods getSupportedTypes(), getSupportedOperations(IdType) and supportsAuthentication().

If you look at the example code, you will see that we implement these methods to declare that we only support the USER identity-type, the operations CREATE, READ and EDIT on these identities, and return true from supportsAuthentication().

	private static final Set SUPPORTED_TYPES;
	static {
		SUPPORTED_TYPES = new HashSet();
		SUPPORTED_TYPES.add(IdType.USER);
	}

	private static final Set SUPPORTED_OPS;
	static {
		SUPPORTED_OPS = new HashSet();
		// TODO: DELETE?  &  SERVICE?
		SUPPORTED_OPS.add(IdOperation.READ);
		SUPPORTED_OPS.add(IdOperation.CREATE);
		SUPPORTED_OPS.add(IdOperation.EDIT);
	}
	
	public Set getSupportedOperations(IdType type) {
		return SUPPORTED_OPS;
	}

	public Set getSupportedTypes() {
		return SUPPORTED_TYPES;
	}

	public boolean supportsAuthentication() {
		return true;
	}

Implementing the Declared Operations

Having declaired that we support CREATE, READ, EDIT and authentication for OpenSSO USER subjects, we need to provide implementations for the relevant methods of IdRepo for OpenSSO to call upon.

Other methods which we must implement (because they are abstract in IdRepo), but which provide unsuppored operations are simply implemented to raise IdRepoUnsupportedOpException.

The operations that we do implement all assert that the IdType argument they are passed is IdType.USER, enforcing the contract we've declared.

SSOToken Arguments

Most of these methods take an initial argument of type SSOToken. This token does not represent the identity upon which the operation in question is being performed (that's what the String 'name' is for). This SSOToken actually references the identity which is performing the operation (i.e. the admin console user, or remote agent).

IdOperation.CREATE

	public String create(SSOToken token, IdType type, String name,
	                     Map attrMap)
		throws IdRepoException, SSOException
	{
		assertUserType(type);
		User newUser = userFromAttributes(name, attrMap);
		try {
			dao.insertUser(newUser);
		} catch (SQLException e) {
			throw new IdRepoException(e.toString());
		}
		// TODO: "string representation of created value" wha?
		return "["+name+"]";
	}

Try to produce a useful return value, although appears that the current implementation of OpenSSO discards it for all IdRepo implementations except its internal one.

IdOperation.READ

	public boolean isExists(SSOToken token, IdType type, String name)
		throws IdRepoException, SSOException
	{
		assertUserType(type);
		try {
			return dao.loadUser(name) != null;
		} catch (SQLException e) {
			throw new IdRepoFatalException(e.toString());
		}
	}
	
	public boolean isActive(SSOToken token, IdType type, String name)
		throws IdRepoException, SSOException
	{
		// We could load the entry from the database to check a status
		// flag, but our simple schema doesn't define one; all user
		// accounts are always enabled. 
		assertUserType(type);
		return true;
	}

	public Map getAttributes(SSOToken token, IdType type, String name)
			throws IdRepoException, SSOException
	{
		assertUserType(type);
		User user;
		try {
			user = dao.loadUser(name);
		} catch (SQLException e) {
			throw new IdRepoException(e.toString());
		}
		return toAttrs(user);
	}

	public Map getAttributes(SSOToken token, IdType type, String name, Set attrNames)
		throws IdRepoException, SSOException
	{
		// TODO: should filter the results to reflect requested attrs,
		// but for the moment, lets just return then all,
		return getAttributes(token, type, name);
	}

	public Map getBinaryAttributes(SSOToken token, IdType type, String name, Set attrNames)
		throws IdRepoException, SSOException
	{
		assertUserType(type);
		// we don't have any binary attributes,
		return Collections.EMPTY_MAP;
	}

	public RepoSearchResults search(SSOToken token, IdType searchType,
			String pattern, int maxTime, int maxResults,
			Set returnAttrs, boolean returnAllAttrs, int filterOp,
			Map avPairs, boolean recursive)
		throws IdRepoException, SSOException
	{
		assertUserType(searchType);
		if ("*".equals(pattern)) {
			// TODO: other 'glob'-like matches allowed?
			pattern = null;
		}
		List users;
		Map queryParams = avPairs==null ? null : remap(avPairs);
		try {
			users = dao.search(pattern, maxResults, queryParams, filterOp);
		} catch (SQLException e) {
			throw new IdRepoException(e.toString());
		}
		Set searchResults = toDNSet(users);
		int errorCode = RepoSearchResults.SUCCESS;
		Map resultsMap = toEntryAttributeSetMap(users);
		return new RepoSearchResults(searchResults,
		                             errorCode,
		                             resultsMap,
		                             searchType);
	}

IdOperation.EDIT

	public void setAttributes(SSOToken token, IdType type, String name, Map attributes, boolean isAdd)
		throws IdRepoException, SSOException
	{
		// TODO: not sure what isAdd indicates
		assertUserType(type);
		try {
			// load user and copy changed properties
			User user = dao.loadUser(name);
			Map props = attrMapToPropMap(attributes);
			copyProperties(user, props);
			dao.updateUser(user);
		} catch (SQLException e) {
			e.printStackTrace();
			throw new IdRepoException(e.toString());
		}
	}

	public void setBinaryAttributes(SSOToken token, IdType type, String name, Map attributes, boolean isAdd)
		throws IdRepoException, SSOException
	{
		assertUserType(type);
		// don't support any binary attributes, so nothing to do
	}

	public void removeAttributes(SSOToken token, IdType type, String name, Set attrNames)
		throws IdRepoException, SSOException
	{
		assertUserType(type);
		// at the moment, there are no attributes that can resonably be
		// removed.  We could in theory set the columns to 'null' in
		// the database, except we've deliberately defined the columns
		// to dissallow this.
	}

Authentication

	public boolean authenticate(Callback[] credentials) throws IdRepoException, AuthLoginException {
		// Obtain user name and password from credentials and authenticate
		String username = null;
		String password = null;
		for (int i = 0; i < credentials.length; i++) {
			Callback cred = credentials[i];
			if (cred instanceof NameCallback) {
				username = ((NameCallback)cred).getName();
			} else if (cred instanceof PasswordCallback) {
				char[] passwd = ((PasswordCallback)cred).getPassword();
				if (passwd != null) {
					password = new String(passwd);
				}
			}
		}
		if (username == null || password == null) {
			Object args[] = { getClass().getName() };
			throw new IdRepoException(IdRepoBundle.BUNDLE_NAME, "221", args);
		}
		User user;
		try {
			user = dao.loadUser(username);
		} catch (SQLException e) {
			throw new IdRepoException(e.toString());
		}
		return user != null && password.equals(user.getPassword());
	}

Implementing the Rest

To see the utility methods used in implementing the public interface above, and the the source code for the Data Access Object, take a look at the full source of JDBCIdRepo, IdDAO, User and PropertyNameRemapper.

Usage patterns

This section attempts to describe how OpenSSO will actually use our plugin to perform different actions.

Authentication

The authentication process is controlled by OpenSSO authentication plugins, not by the the identity repository itself. The section below will show how to configure OpenSSO to check user supplied credentials against our repository plugin using Data Store authentication.

The DataStore auth module requires the user enter a name and password. Once these are collected, it will pass them to our IdRepo to be checked by the authenticate() method. A custom authentication plugin could be built to request alternative credentials, some of which which could then be passed to the IdRepo.

Identity Profile Creation

If, as in the example configuration, we are going to require that users have a profile in the IdRepo, OpenSSO will try to load this after successful auth. The IdRepo API doesn't let OpenSSO ask directly what type of identity as associated with the supplied credentials. OpenSSO will query the reposotory, potentially multiple times, to try and find the named identity under different IdTypes. This means that after a successful authentication, the IdRepo will immidiately see calles to its search() implementation.

Adding the Repository Plugin into the OpenSSO Server

Having created your basic implementation of IdRepo, you will need to 'install' it into an OpenSSO server to see if it works.

In the terminology OpenSSO admin console, we need to add our new code as a 'data store' for our realm.

Avoid the opensso Realm

The OpenSSO server ships with a realm called 'opensso' which is used for managing access to OpenSSO itself (e.g. logging in to the admin console). Do not perform experiments with this realm; instead, create a new sub-realm to avoid locking yourself out of OpenSSO if things go wrong.

To make the plugin available, it's necessary to pack it in to the opensso Web Application Archive file. We can achieve this by adding a fragment of Ant script like the following to our build:

	<target name="repack-opensso-war" depends="package">
		<mkdir dir="build/opensso-tmp"/>
		<unwar src="${opensso-war}" dest="build/opensso-tmp"/>
		<war destfile="build/opensso.war" webxml="build/opensso-tmp/WEB-INF/web.xml">
			<fileset dir="build/opensso-tmp"/>
			<lib file="${idrepo-jarname}"/>
                        <lib file="${jar.hsqldb}"/>
                        <lib file="${jar.beanutils}"/>
		</war>
	</target>

Create a new Realm

We will create a realm to test the IdRepo implementation within. Log in to the admin OpenSSO console and click New.. in 'Realms':

On the subsequent 'New Realm' screen, enter a name for the realm, and select OK, leaving all other settings at their default values:

Adding the Data Store to the new Realm

Select the realm which we just added:

Now, in the configuration screen for the 'test' realm, select the 'Data Stores' tab:

Now click New.. in the 'Data Stores' section:

We are now prompted for the name and type for the new datastore. The type selected must be Access Manager Repository Plug-in. After entering a name, click Next:

The configurables for the repository plugin must now be supplied. On the step 2 screen, enter the name of our IdRepo implementation class. Leaving all other configuration settings as they are, click Finish:

Advanced Data Store Configuration

To make your Data Store a 'first class' entry in the list of Data Store types, and to gain the possiblility of providing for custom parameters to be added via the admin console, the file opensso/products/amserver/xml/services/idRepoService.xml may be updated with a descriptor for your newly implemented data store.

This example is so trivial that it doesn't require any extra configuration data.

If there are any problems loading this plugin, exception stacktraces will appear in $CONFIG_DIR/opensso/debug/amIdm (assuming you have debugging emabled in $CONFIG_DIR/AMConfig.properties).

If, once loaded, the console has problems using the plugin, exception stacktraces will be logged to $CONFIG_DIR/opensso/debug/amConsole

Finally, delete the default flat-file datastore that was created with the realm by ticking the box next to 'files' in the Data Stores list and then clicking Delete

A Little Test of the Data Store

Select the 'Subjects' tab within the 'test' realm configuration. Ensure that things like adding new users and selecting / editing users attiributes works as expected:

Note that the sub-tabs appearing under the 'Subjects' tab should corespond with the values returned by the getSupportedTypes() method of your IdRepo implementation.

Authenticating Against the Data Store

Having added our new datastore, we now need to configure our realm to use this datastore for authentication.

Creating the Authentication Chain

For our 'test' realm, select the 'Authentication' tab:

On the 'Authentication' page, scrolling down past the 'General' section should reveal the 'Module Instances'. Select New for 'Module Instances':

On the New Modual Instance page, enter the name 'testDataStore', select type 'Data Store', and select OK:

Back on the 'test' realm authentication page, we now need to create an authentication chain making use of the authentication module instance we just created. Select New in the 'Authentication Chaining' area:

On the 'New Authentication Chain' page, enter a name, 'testservice' for the new chain, and select OK:

The next page allows us to specify properties for the new chain. There are are initially no entries in the chain, so select Add:

A new entry will be added to the list. This entry should have 'Instance' set to 'testDataStore' and 'Criteria' set to 'REQUIRED'. Select Save and then Back to Authentication to finish the new authentication chain:

Finally, remove the default LDAP authentication configuration by deleting first the 'ldapservice' authentication chain, and then the 'LDAP' module instance:

Zombie LDAP Module

After deleting things as above, I see the LDAP authentication module reappear again in the admin console. I've yet to work out why this is, or if it actually causes any problems.