RIFE logo
This page last changed on Feb 12, 2007 by joshuah@up-bear.net.

Using a database for user data

In the authentication chapter we used so-called memory users, with a list of users in an XML file. When you need a more dynamic list of users, you can use RIFE's support for database users. We'll describe how this differs from memory users somewhat briefly here.

First, we need to make the authentication element extend rife/authenticated/database.xml instead of memory.xml. We also need to specify the configuration file for the data source to use. Let's take a look at an example element file:

Authentication element for database users: elements/auth.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE element SYSTEM "/dtd/element.dtd">

<element extends="rife/authenticated/database.xml">

  <property name="template_name">auth</property>
  <property name="role">admin</property>
  <property name="datasource"><datasource>postgresql</datasource></property>

  <submission name="credentials">
    <param name="login"/>
    <param name="password"/>
  </submission>

  <childtrigger name="authid"/>

</element>
Adding authid and elements/auth.xml to the site file
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE site SYSTEM "/dtd/site.dtd">

<site>

  <!-- Add authid variable -->
  <globalvar name="authid"/>

  <!-- Refer to the AUTH element definition -->
  <element id="AUTH" file="elements/auth.xml" />

  <!-- Indicating that a subsite requires authentication -->
  <subsite id="ADMIN" file="admin.xml" urlprefix="/admin" inherits="AUTH"/>
  ...

</site>

Then you need to create *DatabaseUsers* and *DatabaseSessions* objects using *DatabaseUsersFactory* and *DatabaseSessionsFactory*. RIFE comes with support for many databases (you can find details here), so you don't need to write any code if you use those. Then you call the install methods of the users and sessions objects, after which authentication works just like for memory users.

The installation of the user and session tables has to be performed before we use them. It can be done, for example, in a separate "setup" subsite where the admin is authenticated as a memory user.

Installing the tables for database users
Datasource source = Datasources.getRepInstance().
  getDatasource(Config.getRepInstance().getString("DATASOURCE"));

DatabaseUsersFactory.getInstance(source).install();
DatabaseSessionsFactory.getInstance(source).install();

Alternatively, an example participant to install the tables as well as create a default admin user might look like:

A participant for installing authentication tables
package my.participants;

import java.util.logging.Level;
import java.util.logging.Logger;

import com.uwyn.rife.authentication.credentials.RoleUser;
import com.uwyn.rife.authentication.credentialsmanagers.DatabaseUsers;
import com.uwyn.rife.authentication.credentialsmanagers.DatabaseUsersFactory;
import com.uwyn.rife.authentication.credentialsmanagers.RoleUserAttributes;
import com.uwyn.rife.authentication.sessionmanagers.DatabaseSessionsFactory;
import com.uwyn.rife.database.Datasource;
import com.uwyn.rife.rep.BlockingParticipant;

public class ParticipantDatabaseAuthentication extends
		BlockingParticipant {

	protected void initialize() {
		
		String dsName = getParameter();
		if( null == dsName || 0 == dsName.length() ) {
			dsName="datasource";
		}
		
		Datasource mDatasource = (Datasource)getRepository().getProperties().getValue(dsName);
		
		boolean hasUsers = true;
		DatabaseUsers users = null;
		try {
			users = DatabaseUsersFactory.getInstance(mDatasource);
			users.install();
			hasUsers = false;
		} catch (Exception e) {
			Logger.getLogger(this.getClass().getName()).log(Level.INFO, 
"The Database Authentication tables could not be installed -- they probably already exist.", e);
		}
		
		// If we were able to create a users table, then
		// there aren't going to be any users in it.
		if( !hasUsers ) {
		try {
				// Create an 'admin' role
				String defaultRoleName = "admin";
				users.addRole(defaultRoleName);
				
				// Create a user named 'admin', with password 'password', and role 'admin'
				RoleUser user = new RoleUser();
				user.setLogin("admin");
				user.setPassword("password");
				RoleUserAttributes userAttrs = new RoleUserAttributes();
				userAttrs.setPassword(user.getPassword());
				userAttrs.setRoles(new String[] {defaultRoleName} );
				
				users.addUser(user.getLogin(), userAttrs);
			} catch (Exception e) {
				Logger.getLogger(this.getClass().getName()).log(Level.WARNING, 
"Unable to create a default user.  This will need to be done manually.", e);
			}
		}
		
		try {
			DatabaseSessionsFactory.getInstance(mDatasource).install();
		} catch (Exception e) {
			Logger.getLogger(this.getClass().getName()).log(Level.INFO, 
"The Database Sessions tables could not be installed -- they probably already exist.", e);
		}
	}
}
Participant definition: rep/participants.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE rep SYSTEM "/dtd/rep.dtd">

<rep>
  ...
  <participant param="rep/datasources.xml">ParticipantDatasources</participant>
  <property name="datasource"><datasource>mysql</datasource></property>
  <participant blocking="true" param="datasource">my.participants.ParticipantDatabaseAuthentication</participant>
  ...
</rep>

Mixing memory and database backends

In some cases, you don't want to store session information in the database, even though the users are stored there. That's what the mixed authentication elements is for: rife/authenticated/mixed.xml. It is used in the same way as the database authentication element, with the exception that the database session object isn't needed since the session data will be stored in memory.

Automatically purging sessions

The standard versions of the memory, database and mixed authentication elements automatically purge the session information when it's outdated.

<element extends="rife/authenticated/database.xml">

The maximum time a user can be idle before the session is invalidated is controlled by the configuration option SESSION_DURATION, which defaults to 20 minutes. The value for this configuration parameter should be provided in milliseconds.

You can also customize the frequency of the purging by specifying new values for SESSION_PURGE_FREQUENCY and SESSION_PURGE_SCALE. By default they respectively are 20 and 1000, which means that the purging will have once every fifty times the authentication sessions are accessed.

Creating users

To add users to the database, you will need to use the DatabaseUsers class. A new user needs a login name, a password, and at least one role; the RoleUser and RoleUserAttributes classes hold those values. For example, if you have a registration form that asks the user for a login name and password, the submission handler might look like this:

Creating a new user
/* Fetch the login and password from the form. */
RoleUser user = getSubmissionBean(RoleUser.class);

/* If the login and password are okay, create the user. */
if (user.validate()) {
    /* Fetch the data source for your database in whatever way you like. */
    Datasource ds = (Datasource) getProperty("datasource");

    /* And use it to create a user-management helper object. */
    DatabaseUsers users = DatabaseUsersFactory.getInstance(ds);

    try {
        /* The user should have a password and a single role of "user".
           We could also use the RoleUserAttributes() constructor to avoid
           these "set" calls. */
        RoleUserAttributes attrs = new RoleUserAttributes();
        attrs.setPassword(user.getPassword());
        attrs.setRoles(new String[] { "user" });

        /* Add the user to the database. */
        users.addUser(user.getLogin(), attrs);

        /* Send the user to a "thanks for signing up" page (this assumes
           an exit called "success" is defined for the registration page.) */
        exit("success");
    } catch (CredentialsManagerException e) {
        /* Handle the error; in this example we just log the exception
           and fall through to redisplaying the form. */
        log.severe(ExceptionUtils.getExceptionStackTrace(e));
    }
}

/* Redisplay the signup form with errors. */
generateForm(template, user);
print(template);

Managing additional user data

Of course, most sites need to know more about their users than just a login and password. Since RIFE's authentication manager controls the contents of the user table, we store additional user data in its own class with an associated separate table. (It is possible to replace RIFE's authentication manager with your own, if you want to define your own data model to combine the authentication information and the user's account information; see this mailing list thread for some details.)

In these examples, we will use the Generic query manager to access the user's data.

The RIFE convention is to call the additional class "Account", but of course you are free to choose any class name you like. For example, suppose we want to keep an E-mail address for each user.

Account.java
public class Account {
    /* Account ID. */
    private int id;

    /* RIFE-managed user ID. */
    private long rifeUserId;

    /* The user's E-mail address. */
    private String email;

    ... getters and setters for the above ...
}

Note that we need to store a unique ID for the account separately from the RIFE-supplied user ID. Ideally these would be a single attribute since this is a one-to-one mapping, but RIFE's persistence layer does not currently support the use of the "long" type for identifiers.

The class has some metadata; we use Meta data merging to keep the Account class clean.

AccountMetaData.java
public class AccountMetaData extends MetaData {
    public void activateMetaData() {
        String userTable = RifeConfig.Authentication.getTableUser();

        addConstraint(new ConstrainedProperty("id")
                      .identifier(true)
                      .editable(false));
        addConstraint(new ConstrainedProperty("rifeUserId")
                      .notNull(true)
                      .manyToOne(userTable, "userid"));
        addConstraint(new ConstrainedProperty("email")
                      .notNull(true)
                      .email(true)
                      .maxLength(100));
    }
}

The unusual thing here is the way we set up a foreign key to RIFE's user table. We want to make each Account object refer to a corresponding particular RoleUser object. But RoleUser is not managed by RIFE's constraint-based persistence framework, so we need to explicitly specify the table name in the manyToOne() call. If we try passing in RoleUser.class, RIFE will try to look for a "roleuser" table.

Creating the user

Now we add an "email" field to our registration form. We can let RIFE instantiate both the Account and the RoleUser objects for us based on the fields in the form. The element declaration for our registration form will look like:

Registration form element declaration
<element id="Register" implementation="com.foobar.elements.Register" url="register">
    <submission name="register">
        <bean classname="com.foobar.Account" />
        <bean classname="com.uwyn.rife.authentication.credentials.RoleUser" />
    </submission>
</element>

And the submission handler for the registration form now looks like:

User and account creation
public void doRegister() {
    RoleUser user = getSubmissionBean(RoleUser.class);

    /* Fetch the email address from the form. */
    Account account = getSubmissionBean(Account.class);

    /* If the login, password, and email are okay, create the user. */
    if (user.validate() && ((Validated)account).validate()) {
        Datasource ds = (Datasource) getProperty("datasource");
        DatabaseUsers users = DatabaseUsersFactory.getInstance(ds);

        /* We will also need to store the account, so get a query manager for it. */
        ContentQueryManager<Account> accountManager =
                new ContentQueryManager<Account>(ds, Account.class);

        try {
            /* Create the user as in the previous example (but more tersely). */
            users.addUser(user.getLogin(), new RoleUserAttributes(user.getPassword(),
                                                              new String[] { "user" }));

            /* The user is created. Now we can fetch its ID and remember it in the
               Account object. */
            account.setRifeUserId(users.getUserId(user.getLogin()));

            /* Store the new account in the database. */
            accountManager.save(account);

            exit("success");
        } catch (CredentialsManagerException e) {
            /* Handle the error; in this example we just log the exception
               and fall through to redisplaying the form. */
            log.severe(ExceptionUtils.getExceptionStackTrace(e));
        }
    }

    /* Redisplay the signup form with errors. */
    generateForm(template, user);
    generateForm(template, account); 
    print(template);
}

Most likely you will want to create the RIFE user and your Account object in a transaction, so that you don't end up with a half-created user if your Account creation fails for some reason. See the cookbook page on Chainable transactions for information about that.

Accessing the additional data

Now that we've added an Account row for this user, we need to read it back in when an element needs to make use of it. Assuming our element requires authentication, or extends identified.xml (see User identification facility in the Cookbook), we can use the Generic query manager to fetch the user's Account object. The code looks like this:

Fetching the Account object
RoleUserIdentity identity;
Account account = null;

/* Since this is an identified or authenticated element, there is a
   RoleUserIdentity assigned to the request if the user is logged in. */
identity = (RoleUserIdentity)getRequestAttribute(Identified.IDENTITY_ATTRIBUTE_NAME);

/* Anonymous users can hit identified (but not authentication-required) pages.
   Don't try to fetch their account data. */
if (identity != null)
{
    GenericQueryManager<Account> manager;

    /* Fetch the data source using whatever technique you like. */
    Datasource ds = (Datasource) getProperty("datasource");
    manager = GenericQueryManagerFactory.getInstance(ds, Account.class);

    /* Now we fetch the account using a simple query manager call.
       The RoleUserIdentity has a child object which contains the user ID. */
    account = mgr.restoreFirst(mgr.getRestoreQuery()
                                  .where("rifeUserId", "=",
			                 identity.getAttributes().getUserId()));
}

Database schema

If you need to manually create the tables used by the database authentication backend (e.g. to control their layout on disk), or you just want to examine them, here are their definitions. The exact data types of the columns varies depending on the particular database you're using. Note that these are the default table names; the defaults may be overridden as noted below.

authentication

This table contains details about all the active sessions. It is not used if you're using the "mixed" backend described above. As users log in and out, rows are inserted and deleted from this table. All columns are NOT NULL. Table name may be overridden by the TABLE_AUTHENTICATION config parameter.

authid varchar(32) Opaque session identifier, as stored in the authid cookie or request parameter. Primary key.
userid integer (64-bit) User ID.
hostip varchar(40) Remote IP address of the session.
sessstart integer (64-bit) Session start time, in milliseconds since the epoch (System.currentTimeMillis() format). Indexed (non-unique) to allow fast deletion of expired sessions.
remembered boolean True if the user should be remembered after the sesion expires.

authrole

This table contains the list of known authentication roles. All columns are NOT NULL. Table name may be overridden by the TABLE_ROLE config parameter.

roleid integer (32-bit) Unique numeric identifier of the role. Primary key.
name varchar(20) Name of the role. Has a unique constraint.

authrolelink

Link table between the authrole and authuser tables; defines which users have which roles. All columns are NOT NULL. Table name may be overridden by the TABLE_ROLELINK config parameter.

userid integer (64-bit) User ID, foreign key to authuser.userid. First part of composite primary key.
roleid integer (32-bit) Which role the user has, foreign key to authrole.roleid with cascading deletes enabled. Second part of composite primary key.

authuser

List of known users and their passwords. All columns are NOT NULL. Table name may be overridden by the TABLE_USER config parameter.

userid integer (64-bit) User ID. Primary key.
login varchar(20) User's login name. Has a unique constraint.
passwd varchar(100) User's password, possibly encrypted (see Password encryption).
Document generated by Confluence on Oct 19, 2010 14:56