Sesat > Docs + Support > Architecture Overview > Personalization architecture

SESAT Personalization architecture

This is a document describing the design of and how the personalization architecture works in Sesam.

Components / API

The architecture for personalization is build on three systems:

  • the search front
  • the user admin application
  • the RMI backend, the service layer, for handling lookup and persistens of user data.

Both the RMI backend and the user admin application runs on our HA-JBoss environment using the HA-mysql database. The search front and the user admin application must share domain so they can share login cookies.

Search Front

The search front should be as clean as possible. That's why we want to separate the user admin into it's own application. Then we don't have to handle validation and stuff like that in the search front.

The search front will contain a user filter that is used to check if there exists a known user and populate the data model with the user object and the belonging properties. The search front can also set and remove single properties for a user, to make it possible handling i.e. news cases that is implemented as drag-n-drop. And a logout method. For more advanced editing of properties and creating user accounts etc, a link will bring the user over to the admin application.

The user filter will work something like this.

UserFilter.java
public void doFilter(..) {

    if (null == datamodel.getUser()) {

        // Look for login cookie

        if (null != cookie) {

            // Lookup user data by using cookie

            if (null == user) {

                // Do nothing, end filter

            }

            if (legalToken) {

                // Populate data model with user data and belonging properties
                // Update login cookie

            } else {

                // Invalidate all logins, warn user of theft

            }
        }
    }
}

User Admin Application

The user admin application should have the following functionality:

  • Create User
    Form for creating a new user. Submit will create a non-confirmed user object and send confirm mail to the email address that is submitted.
  • Confirm User
    Just a direct access page for confirming a new user. This page will confirm the user in the database and redirect the user to the login page.
  • Login User
    Page where the user can log in. After login, get links back to Sesam or update user data.
  • Edit Properties
    Pages where the user can edit all the properties for his user.
  • Edit User
    Page where the user can update the user info. If the user is based on an ldap user, the update will happen automatically, and give a message if an update was done.
  • Delete User
    Users should have the possiblility to delete their user object and all the belonging information.
  • Master Logout
    This action erases all remembered logins for the user in one action.

RMI backend

The RMI backend should contain the implementation for the EJB3 entity beans, and a service layer that is used by the different applications. The service layer should contain all needed functionality, in an easy accessible way.

The basic user service is a lightweight service class for authenticating users, and an easy way of getting/setting properties.

BasicUserService.java
public interface BacisUserService {

    BasicUser authenticateByLoginKey(final String loginKey) throws InvalidTokenException;

    BasicUser authenticateByUsername(final String username, final String password);

    BasicUser findBasicUserByUsername(final String username);

    BasicUser refreshUser(final BasicUser user);

    void invalidateLogin(final String loginKey);

    void invalidateAllLogins(final String loginKey);

    void invalidateAllLogins(final BasicUser user);

    BasicUser createUser(final String email, final String firstName, final String lastName,
        final String password, final String confirmUrl) throws UserAlreadyExistsException,
        MailException;

    boolean confirmUser(final BasicUser user, final String confirmKey);

    void deleteUser(final BasicUser user);

    BasicUser setUserProperty(final BasicUser user, final PropertyKey propertyKey,
        final String propertyValue);

    BasicUser removeUserProperty(final BasicUser user, final PropertyKey propertyKey);

    boolean isLegalLoginKey(final String loginKey);

}

The basic user class is a lightweight value class that just wraps in the needed information, totally stripped for entity magic. Remoting and serialization is also easy since it just contains simple Java attributes.

BasicUser.java
public interface BasicUser extends Serializable {

    Long getUserId();

    String getUsername();

    String getFirstName();

    String getLastName();

    Date getCreated();

    Date getLastLogin();

    boolean isExternal();

    Map<PropertyKey, String> getUserPropertiesMap();

    String getNextLoginKey();

    void setNextLoginKey(final String nextLoginKey);

    Date getUpdateTimestamp();

    void setUpdateTimestamp(final Date updateTimestamp);

    boolean isDirty(final Date timestamp);

}

Persistent Login Cookie

Here is a summary of how we're handling the login cookies for Sesam.

Our solution is based on:
Persistent Login Cookie Best Practice

With a posted improvement:
Improved Persistent Login Cookie Best Practice

The summary:

  1. When the user successfully logs in with Remember Me checked, a login cookie is issued in addition to the standard session management cookie.
  2. The login cookie contains the user's username and a random number (the "token" from here on) from a suitably large space. The username and token are stored as a pair in a database table.
  3. When a non-logged-in user visits the site and presents a login cookie, the username and token are looked up in the database.
    1. If the pair is present, the user is considered authenticated. The used token is removed from the database. A new token is generated, stored in database with the username, and issued to the user via a new login cookie.
    2. If the pair is not present, the login cookie is ignored.
  4. Users that are only authenticated via this mechanism are not permitted to access certain protected information or functions such as changing a password, viewing personally identifying information, or spending money. To perform those operations, the user must first successfully submit a normal username/password login form.
  5. Since this approach allows the user to have multiple remembered logins from different browsers or computers, a mechanism is provided for the user to erase all remembered logins in a single operation.

The problem:

One disadvantage, however, is that if an attacker successfully steals a victim's login cookie and uses it before the victim next accesses the site, the cookie will work and the site will issue a new valid login cookie to the attacker (this disadvantage is far from unique to Miller's design). The attacker will be able to continue accessing the site as the victim until the remembered login session expires. When the victim next accesses the site his remembered login will not work (because each token can only be used one time) but he's much more likely to think that "something broke" and just log in again than to realize that his credentials were stolen.

The improvement:

  1. When the user successfully logs in with Remember Me checked, a login cookie is issued in addition to the standard session management cookie.
  2. The login cookie contains the user's username, a series identifier, and a token. The series and token are unguessable random numbers from a suitably large space. All three are stored together in a database table.
  3. When a non-logged-in user visits the site and presents a login cookie, the username, series, and token are looked up in the database.
    1. If the triplet is present, the user is considered authenticated. The used token is removed from the database. A new token is generated, stored in database with the username and the same series identifier, and a new login cookie containing all three is issued to the user.
    2. If the username and series are present but the token does not match, a theft is assumed. The user receives a strongly worded warning and all of the user's remembered sessions are deleted.
    3. If the username and series are not present, the login cookie is ignored.

Comments

  1. The cookie values would be on the form userId###series###token (specification below)
    Example: 1234###550E8400-E29B-11D4-A716-446655440000###3051a8d7-aea7-1801-e0bf-bc539dd60cf3
  2. Users can have several user cookies, so they can be logged in on different machines at the same time.
  3. There should be a service that will delete and clean up old data. Login cookies not used within 3 months, and users not accessed within 1 year should be deleted? Could be implemented by a scheduled service in the backend.

Database

Below follow the SQL for the entities we use for personalization.

USER
create table USER (
  USER_ID bigint(20) primary key auto_increment,
  USERNAME varchar(50) not null,
  PASSWORD_HASH varchar(50) not null,
  PASSWORD_SALT varchar(10) not null,
  FIRST_NAME varchar(50) not null,
  LAST_NAME varchar(50) not null,
  CREATED timestamp not null,
  CONFIRMED timestamp null,
  LAST_UPDATED timestamp not null,
  LAST_LOGIN timestamp null,
  EXTERNAL char(1) not null  /* T or F */
) engine=InnoDB charset=utf8;

/* The username should be a mail address for internal users, and the ldap username or email for external users. */
/* Passwords should not be saved in clear text, but as a hash. */
/* For ldap users, the password could be just set to i.e. "external", not hashed. */

create index USER_ID on USER(USER_ID);

create unique index USERNAME on USER(USERNAME);

create index USERNAME_PASSWORD_HASH on USER(USERNAME, PASSWORD_HASH);

create index FIRST_NAME on USER(FIRST_NAME);

create index LAST_NAME on USER(LAST_NAME);
USER_PROPERTY
create table USER_PROPERTY (
  USER_PROPERTY_ID bigint(20) primary key auto_increment,
  USER_ID bigint(20) not null,
  PROPERTY_KEY varchar(50) not null,
  PROPERTY_VALUE varchar(2048)
) engine=InnoDB charset=utf8;

create index USER_ID on USER_PROPERTY(USER_ID);

create index PROPERTY_KEY on USER_PROPERTY(PROPERTY_KEY);

create index USER_ID_PROPERTY_KEY on USER_PROPERTY(USER_ID, PROPERTY_KEY);

alter table USER_PROPERTY
  add foreign key (USER_ID) references USER(USER_ID);
USER_COOKIE
create table USER_COOKIE (
  USER_COOKIE_ID bigint(20) primary key auto_increment,
  USER_ID bigint(20) not null,
  SERIES varchar(50) not null,
  TOKEN varchar(50) not null,
  CREATED timestamp not null
) engine=InnoDB charset=utf8;

create index USER_ID on USER_COOKIE(USER_ID);

create unique index SERIES on USER_COOKIE(SERIES);

create index USER_ID_SERIES on USER_COOKIE(USER_ID, SERIES);

create index CREATED on USER_COOKIE(CREATED);

alter table USER_COOKIE
  add foreign key (USER_ID) references USER(USER_ID);

Comments

  1. Legal property keys is implemented by using a Java enum. Can also be implemented as a code value entity.
  2. When creating cookie series, must check for duplicated series id's. Must be done synchronized?
 © 2007-2009 Schibsted ASA
Contact us