| Sesat > Docs + Support > Architecture Overview > Personalization architecture |
This is a document describing the design of and how the personalization architecture works in Sesam.
The architecture for personalization is build on three systems:
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.
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.
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 } } } }
The user admin application should have the following functionality:
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.
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.
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); }
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:
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:
Comments
Below follow the SQL for the entities we use for personalization.
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);
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);
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