Table of Contents
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 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).
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.
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 }
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; }
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.
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).
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.
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); }
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. }
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()); }
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.
This section attempts to describe how OpenSSO will actually use our plugin to perform different actions.
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.
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.
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.
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>
We will create a realm to test the IdRepo implementation within. Log in to the admin OpenSSO console and click
in 'Realms':On the subsequent 'New Realm' screen, enter a name for the realm, and select
, leaving all other settings at their default values:Select the realm which we just added:
Now, in the configuration screen for the 'test' realm, select the 'Data Stores' tab:
Now click
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 :
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
: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
(assuming you have debugging emabled in
$CONFIG_DIR
/opensso/debug/amIdm
).$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
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.
Having added our new datastore, we now need to configure our realm to use this datastore for authentication.
For our 'test' realm, select the 'Authentication' tab:
On the 'Authentication' page, scrolling down past the 'General' section should reveal the 'Module Instances'. Select
for 'Module Instances':On the New Modual Instance page, enter the name 'testDataStore', select type 'Data Store', and select
: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
in the 'Authentication Chaining' area:On the 'New Authentication Chain' page, enter a name, 'testservice' for the new chain, and select
:The next page allows us to specify properties for the new chain. There are are initially no entries in the chain, so select
:A new entry will be added to the list. This entry should have 'Instance' set to 'testDataStore' and 'Criteria' set to 'REQUIRED'. Select
and then 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:
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.