diff --git a/src/java/com/samskivert/servlet/user/Authenticator.java b/src/java/com/samskivert/servlet/user/Authenticator.java index 979bb750..b3fd725e 100644 --- a/src/java/com/samskivert/servlet/user/Authenticator.java +++ b/src/java/com/samskivert/servlet/user/Authenticator.java @@ -37,13 +37,10 @@ public interface Authenticator * repository against which the user-supplied data is to be checked. * @param username the username supplied by the user. * @param password the plaintext password supplied by the user. - * @param persist if true, the cookie will expire in one month, if - * false, the cookie will expire in 24 hours. * * @throws AuthenticationFailedException if the user failed to pass * the authentication check. */ - public void authenticateUser ( - User user, String username, Password password, boolean persist) + public void authenticateUser (User user, String username, Password password) throws AuthenticationFailedException; } diff --git a/src/java/com/samskivert/servlet/user/UserManager.java b/src/java/com/samskivert/servlet/user/UserManager.java index 21d3a8a1..f17a2ea2 100644 --- a/src/java/com/samskivert/servlet/user/UserManager.java +++ b/src/java/com/samskivert/servlet/user/UserManager.java @@ -36,9 +36,8 @@ import com.samskivert.util.StringUtil; import com.samskivert.util.Tuple; /** - * The user manager provides easy access to user objects for servlets. It - * takes care of cookie management involved in login, logout and loading a - * user record during an authenticated session. + * The user manager provides easy access to user objects for servlets. It takes care of cookie + * management involved in login, logout and loading a user record during an authenticated session. */ public class UserManager { @@ -51,16 +50,14 @@ public class UserManager new PasswordAuthenticator(); /** - * A totally insecure authenticator that authenticates any user. - * Note: Applications that make use of this authenticator - * should make sure the user has already been authenticated through - * some other means. + * A totally insecure authenticator that authenticates any user. Note: Applications + * that make use of this authenticator should make sure the user has already been authenticated + * through some other means. */ public static class InsecureAuthenticator implements Authenticator { // documentation inherited - public void authenticateUser ( - User user, String username, Password password, boolean persist) + public void authenticateUser (User user, String username, Password password) throws InvalidPasswordException { // don't care @@ -68,14 +65,13 @@ public class UserManager } /** - * An authenticator that requires that the user-supplied password - * match the actual user password. + * An authenticator that requires that the user-supplied password match the actual user + * password. */ public static class PasswordAuthenticator implements Authenticator { // documentation inherited - public void authenticateUser ( - User user, String username, Password password, boolean persist) + public void authenticateUser (User user, String username, Password password) throws AuthenticationFailedException { if (!user.passwordsMatch(password)) { @@ -85,27 +81,25 @@ public class UserManager } /** - * Prepares this user manager it for operation. Presently the user manager - * requires the following configuration information: + * Prepares this user manager it for operation. Presently the user manager requires the + * following configuration information: * * * * @param config the user manager configuration properties. - * @param conprov the database connection provider that will be used - * to obtain a connection to the user database. + * @param conprov the database connection provider that will be used to obtain a connection to + * the user database. */ public void init (Properties config, ConnectionProvider conprov) throws PersistenceException @@ -119,8 +113,7 @@ public class UserManager // fetch the login URL from the properties _loginURL = config.getProperty("login_url"); if (_loginURL == null) { - Log.warning("No login_url supplied in user manager config. " + - "Authentication won't work."); + Log.warning("No login_url supplied in user manager config. Authentication won't work."); } // look up any override to our user auth cookie @@ -159,12 +152,11 @@ public class UserManager } /** - * Fetches the necessary authentication information from the http - * request and loads the user identified by that information. + * Fetches the necessary authentication information from the http request and loads the user + * identified by that information. * - * @return the user associated with the request or null if no user was - * associated with the request or if the authentication information is - * bogus. + * @return the user associated with the request or null if no user was associated with the + * request or if the authentication information is bogus. */ public User loadUser (HttpServletRequest req) throws PersistenceException @@ -182,11 +174,10 @@ public class UserManager } /** - * Fetches the necessary authentication information from the http - * request and loads the user identified by that information. If no - * user could be loaded (because the requester is not authenticated), - * a redirect exception will be thrown to redirect the user to the - * login page specified in the user manager configuration. + * Fetches the necessary authentication information from the http request and loads the user + * identified by that information. If no user could be loaded (because the requester is not + * authenticated), a redirect exception will be thrown to redirect the user to the login page + * specified in the user manager configuration. * * @return the user associated with the request. */ @@ -206,29 +197,25 @@ public class UserManager } /** - * Attempts to authenticate the requester and initiate an authenticated - * session for them. An authenticated session involves their receiving a - * cookie that proves them to be authenticated and an entry in the session - * database being created that maps their information to their userid. If - * this call completes, the session was established and the proper cookies - * were set in the supplied response object. If invalid authentication - * information is provided or some other error occurs, an exception will be - * thrown. + * Attempts to authenticate the requester and initiate an authenticated session for them. An + * authenticated session involves their receiving a cookie that proves them to be authenticated + * and an entry in the session database being created that maps their information to their + * userid. If this call completes, the session was established and the proper cookies were set + * in the supplied response object. If invalid authentication information is provided or some + * other error occurs, an exception will be thrown. * * @param username The username supplied by the user. * @param password The password supplied by the user. - * @param persist If true, the cookie will expire in one month, if false, - * the cookie will expire at the end of the user's browser session. + * @param persist If true, the cookie will expire in one month, if false, the cookie will + * expire at the end of the user's browser session. * @param req The request via which the login page was loaded. * @param rsp The response in which the cookie is to be set. - * @param auth The authenticator used to check whether the user should be - * authenticated. + * @param auth The authenticator used to check whether the user should be authenticated. * * @return the user object of the authenticated user. */ public User login (String username, Password password, boolean persist, - HttpServletRequest req, HttpServletResponse rsp, - Authenticator auth) + HttpServletRequest req, HttpServletResponse rsp, Authenticator auth) throws PersistenceException, AuthenticationFailedException { // load up the requested user @@ -245,31 +232,29 @@ public class UserManager } // run the user through the authentication gamut - auth.authenticateUser(user, username, password, persist); + auth.authenticateUser(user, username, password); // give them the necessary cookies and business - effectLogin(user, persist, req, rsp); + effectLogin(user, persist ? PERSIST_EXPIRE_DAYS : NON_PERSIST_EXPIRE_DAYS, req, rsp); return user; } /** - * Attempts to authenticate the requester and initiate an authenticated - * session for them. A session token will be assigned to the user and - * returned along with the associated {@link User} record. It is assumed - * that the client will maintain the session token via its own means. + * Attempts to authenticate the requester and initiate an authenticated session for them. A + * session token will be assigned to the user and returned along with the associated {@link + * User} record. It is assumed that the client will maintain the session token via its own + * means. * - * @param username The username supplied by the user. - * @param password The password supplied by the user. - * @param persist If true, the cookie will expire in one month, if false, - * the cookie will expire at the end of the user's browser session. - * @param auth The authenticator used to check whether the user should be - * authenticated. + * @param username the username supplied by the user. + * @param password the password supplied by the user. + * @param expires the number of days in which this session should expire. + * @param auth the authenticator used to check whether the user should be authenticated. * * @return the user object of the authenticated user. */ - public Tuple login (String username, Password password, - boolean persist, Authenticator auth) + public Tuple login ( + String username, Password password, int expires, Authenticator auth) throws PersistenceException, AuthenticationFailedException { // load up the requested user @@ -286,37 +271,33 @@ public class UserManager } // run the user through the authentication gamut - auth.authenticateUser(user, username, password, persist); + auth.authenticateUser(user, username, password); // register a session for this user - String authcode = _repository.registerSession(user, persist); + String authcode = _repository.registerSession(user, expires); return new Tuple(user, authcode); } /** - * If a user is already known to be authenticated for one reason or - * other, this method can be used to give them the appropriate - * authentication cookies to effect their login. + * If a user is already known to be authenticated for one reason or other, this method can be + * used to give them the appropriate authentication cookies to effect their login. + * + * @param expires the number of days in which to expire the session cookie, 0 means expire at + * the end of the browser session. */ - public void effectLogin (User user, boolean persist, - HttpServletRequest req, HttpServletResponse rsp) + public void effectLogin ( + User user, int expires, HttpServletRequest req, HttpServletResponse rsp) throws PersistenceException { - // register a session for this user - String authcode = _repository.registerSession(user, persist); - // stick it into a cookie for their browsing convenience + String authcode = _repository.registerSession(user, Math.max(expires, 1)); Cookie acookie = new Cookie(_userAuthCookie, authcode); - // strip the hostname from the server and use that as the domain - // unless configured not to - if (!"false".equalsIgnoreCase( - _config.getProperty("auth_cookie.strip_hostname"))) { + // strip the hostname from the server and use that as the domain unless configured not to + if (!"false".equalsIgnoreCase(_config.getProperty("auth_cookie.strip_hostname"))) { CookieUtil.widenDomain(req, acookie); } acookie.setPath("/"); - // expire in one month if persistent, else at the end of the - // session - acookie.setMaxAge((persist) ? (30*24*60*60) : -1); + acookie.setMaxAge((expires > 0) ? (expires*24*60*60) : -1); rsp.addCookie(acookie); } @@ -378,4 +359,10 @@ public class UserManager /** Prune the session table every hour. */ protected static final long SESSION_PRUNE_INTERVAL = 60L * 60L * 1000L; + + /** Indicates how long (in days) that a "persisting" session token should last. */ + protected static final int PERSIST_EXPIRE_DAYS = 30; + + /** Indicates how long (in days) that a "non-persisting" session token should last. */ + protected static final int NON_PERSIST_EXPIRE_DAYS = 1; } diff --git a/src/java/com/samskivert/servlet/user/UserRepository.java b/src/java/com/samskivert/servlet/user/UserRepository.java index 70ef87eb..93e25859 100644 --- a/src/java/com/samskivert/servlet/user/UserRepository.java +++ b/src/java/com/samskivert/servlet/user/UserRepository.java @@ -47,24 +47,21 @@ import com.samskivert.util.Tuple; import com.samskivert.servlet.SiteIdentifier; /** - * Interfaces with the RDBMS in which the user information is stored. The - * user repository encapsulates the creating, loading and management of - * users and sessions. + * Interfaces with the RDBMS in which the user information is stored. The user repository + * encapsulates the creating, loading and management of users and sessions. */ public class UserRepository extends JORARepository { /** - * The database identifier used to obtain a connection from our - * connection provider. The value is userdb which you'll - * probably need to know to provide the proper configuration to your - * connection provider. + * The database identifier used to obtain a connection from our connection provider. The value + * is userdb which you'll probably need to know to provide the proper + * configuration to your connection provider. */ public static final String USER_REPOSITORY_IDENT = "userdb"; /** - * Creates the repository and opens the user database. The database - * identifier used to fetch our database connection is documented by - * {@link #USER_REPOSITORY_IDENT}. + * Creates the repository and opens the user database. The database identifier used to fetch + * our database connection is documented by {@link #USER_REPOSITORY_IDENT}. * * @param provider the database connection provider. */ @@ -81,16 +78,15 @@ public class UserRepository extends JORARepository * @param password the password for the new user. * @param realname the user's real name. * @param email the user's email address. - * @param siteId the unique identifier of the site through which this - * account is being created. The resulting user will be tracked as - * originating from this site for accounting purposes ({@link - * SiteIdentifier#DEFAULT_SITE_ID} can be used by systems that don't - * desire to perform site tracking. + * @param siteId the unique identifier of the site through which this account is being + * created. The resulting user will be tracked as originating from this site for accounting + * purposes ({@link SiteIdentifier#DEFAULT_SITE_ID} can be used by systems that don't desire to + * perform site tracking. * * @return The userid of the newly created user. */ - public int createUser (Username username, Password password, - String realname, String email, int siteId) + public int createUser ( + Username username, Password password, String realname, String email, int siteId) throws UserExistsException, PersistenceException { // create a new user object... @@ -106,8 +102,7 @@ public class UserRepository extends JORARepository /** * Looks up a user by username. * - * @return the user with the specified user id or null if no user with - * that id exists. + * @return the user with the specified user id or null if no user with that id exists. */ public User loadUser (String username) throws PersistenceException @@ -118,8 +113,7 @@ public class UserRepository extends JORARepository /** * Looks up a user by userid. * - * @return the user with the specified user id or null if no user with - * that id exists. + * @return the user with the specified user id or null if no user with that id exists. */ public User loadUser (int userId) throws PersistenceException @@ -130,15 +124,14 @@ public class UserRepository extends JORARepository /** * Looks up a user by a session identifier. * - * @return the user associated with the specified session or null of - * no session exists with the supplied identifier. + * @return the user associated with the specified session or null of no session exists with the + * supplied identifier. */ public User loadUserBySession (String sessionKey) throws PersistenceException { - User user = load(_utable, "sessions", - "where authcode = '" + sessionKey + "' AND " + - "sessions.userId = users.userId"); + User user = load(_utable, "sessions", "where authcode = '" + sessionKey + "' " + + "AND sessions.userId = users.userId"); if (user != null) { user.setDirtyMask(_utable.getFieldMask()); } @@ -165,12 +158,10 @@ public class UserRepository extends JORARepository } /** - * Lookup a user by email address, something that is not efficient and - * should really only be done by site admins attempting to look up a - * user record. + * Lookup a user by email address, something that is not efficient and should really only be + * done by site admins attempting to look up a user record. * - * @return the user with the specified user id or null if no user with - * that id exists. + * @return the user with the specified user id or null if no user with that id exists. */ public ArrayList lookupUsersByEmail (String email) throws PersistenceException @@ -179,12 +170,11 @@ public class UserRepository extends JORARepository } /** - * Looks up a list of users that match an arbitrary query. Care should be - * taken in constructing these queries as the user table is likely to be - * large and a query that does not make use of indices could be very slow. + * Looks up a list of users that match an arbitrary query. Care should be taken in constructing + * these queries as the user table is likely to be large and a query that does not make use of + * indices could be very slow. * - * @return the users matching the specified query or an empty list if there - * are no matches. + * @return the users matching the specified query or an empty list if there are no matches. */ public ArrayList lookupUsersWhere (final String where) throws PersistenceException @@ -198,13 +188,12 @@ public class UserRepository extends JORARepository } /** - * Updates a user that was previously fetched from the repository. - * Only fields that have been modified since it was loaded will be - * written to the database and those fields will subsequently be - * marked clean once again. + * Updates a user that was previously fetched from the repository. Only fields that have been + * modified since it was loaded will be written to the database and those fields will + * subsequently be marked clean once again. * - * @return true if the record was updated, false if the update was - * skipped because no fields in the user record were modified. + * @return true if the record was updated, false if the update was skipped because no fields in + * the user record were modified. */ public boolean updateUser (final User user) throws PersistenceException @@ -218,19 +207,16 @@ public class UserRepository extends JORARepository } /** - * 'Delete' the users account such that they can no longer access it, - * however we do not delete the record from the db. The name is - * changed such that the original name has XX=FOO if the name were FOO - * originally. If we have to lop off any of the name to get our - * prefix to fit we use a minus sign instead of a equals side. The - * password field is set to be the empty string so that no one can log - * in (since nothing hashes to the empty string. We also make sure - * their email address no longer works, so in case we don't ignore - * 'deleted' users when we do the sql to get emailaddresses for the mass - * mailings we still won't spam delete folk. We leave the emailaddress - * intact exect for the @ sign which gets turned to a #, so that we can - * see what their email was incase it was an accidently deletion and we - * have to verify through email. + * 'Delete' the users account such that they can no longer access it, however we do not delete + * the record from the db. The name is changed such that the original name has XX=FOO if the + * name were FOO originally. If we have to lop off any of the name to get our prefix to fit we + * use a minus sign instead of a equals side. The password field is set to be the empty string + * so that no one can log in (since nothing hashes to the empty string. We also make sure + * their email address no longer works, so in case we don't ignore 'deleted' users when we do + * the sql to get emailaddresses for the mass mailings we still won't spam delete folk. We + * leave the emailaddress intact exect for the @ sign which gets turned to a #, so that we can + * see what their email was incase it was an accidently deletion and we have to verify through + * email. */ public void deleteUser (final User user) throws PersistenceException @@ -259,8 +245,7 @@ public class UserRepository extends JORARepository String oldName = user.username; for (int ii = 0; ii < 100; ii++) { try { - user.username = StringUtil.truncate( - ii + "=" + oldName, 24); + user.username = StringUtil.truncate(ii + "=" + oldName, 24); _utable.update(conn, user, mask); return null; // nothing to return } catch (SQLException se) { @@ -277,38 +262,27 @@ public class UserRepository extends JORARepository } /** - * Creates a new session for the specified user and returns the randomly - * generated session identifier for that session. If a session entry - * already exists for the specified user it will be reused. + * Creates a new session for the specified user and returns the randomly generated session + * identifier for that session. If a session entry already exists for the specified user it + * will be reused. * - *

Temporary sessions are set to expire in two days (the assumption is - * that the browser will be given a non-persistent cookie and we should - * prune the session table entry after some reasonable amount of time), - * persistent sessions expire after one month. + * @param expireDays the number of days in which the session token should expire. */ - public String registerSession (final User user, boolean persist) + public String registerSession (User user, int expireDays) throws PersistenceException { - // figure out when to expire the session - Calendar cal = Calendar.getInstance(); - cal.add(Calendar.DATE, persist ? 30 : 2); - final Date expires = new Date(cal.getTime().getTime()); - // look for an existing session for this user - Tuple session = execute( - new Operation>() { - public Tuple invoke ( - Connection conn, DatabaseLiaison liaison) + final String query = "select sessionId, authcode from sessions " + + "where userId = " + user.userId; + Tuple session = execute(new Operation>() { + public Tuple invoke (Connection conn, DatabaseLiaison liaison) throws PersistenceException, SQLException { Statement stmt = conn.createStatement(); try { - String query = "select sessionId, authcode from sessions " + - "where userId = " + user.userId; ResultSet rs = stmt.executeQuery(query); while (rs.next()) { - return new Tuple( - rs.getInt(1), rs.getString(2)); + return new Tuple(rs.getInt(1), rs.getString(2)); } return null; } finally { @@ -317,6 +291,11 @@ public class UserRepository extends JORARepository } }); + // figure out when to expire the session + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.DATE, expireDays); + Date expires = new Date(cal.getTime().getTime()); + // if we found one, update its expires time and reuse it if (session != null) { update("update sessions set expires = '" + expires + "' " + @@ -326,11 +305,29 @@ public class UserRepository extends JORARepository // otherwise create a new one and insert it into the table String authcode = UserUtil.genAuthCode(user); - update("insert into sessions (authcode, userId, expires) values('" + - authcode + "', " + user.userId + ", '" + expires + "')"); + update("insert into sessions (authcode, userId, expires) values('" + authcode + "', " + + user.userId + ", '" + expires + "')"); return authcode; } + /** + * Validates that the supplied session key is still valid and if so, refreshes it for the + * specified number of days. + * + * @return true if the session was located and refreshed, false if it no longer exists. + */ + public boolean refreshSession (String sessionKey, int expireDays) + throws PersistenceException + { + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.DATE, expireDays); + Date expires = new Date(cal.getTime().getTime()); + + // attempt to update an existing session row, returning true if we found and updated it + return (update("update sessions set expires = '" + expires + "' " + + "where sessionId = " + sessionKey) == 1); + } + /** * Prunes any expired sessions from the sessions table. */ @@ -341,9 +338,8 @@ public class UserRepository extends JORARepository } /** - * Loads the usernames of the users identified by the supplied user - * ids and returns them in an array. If any users do not exist, their - * slot in the array will contain a null. + * Loads the usernames of the users identified by the supplied user ids and returns them in an + * array. If any users do not exist, their slot in the array will contain a null. */ public String[] loadUserNames (int[] userIds) throws PersistenceException @@ -352,9 +348,8 @@ public class UserRepository extends JORARepository } /** - * Loads the real names of the users identified by the supplied user - * ids and returns them in an array. If any users do not exist, their - * slot in the array will contain a null. + * Loads the real names of the users identified by the supplied user ids and returns them in an + * array. If any users do not exist, their slot in the array will contain a null. */ public String[] loadRealNames (int[] userIds) throws PersistenceException @@ -397,12 +392,11 @@ public class UserRepository extends JORARepository } /** - * Configures the supplied user record with the provided information - * in preparation for inserting the record into the database for the - * first time. + * Configures the supplied user record with the provided information in preparation for + * inserting the record into the database for the first time. */ - protected void populateUser (User user, Username name, Password pass, - String realname, String email, int siteId) + protected void populateUser ( + User user, Username name, Password pass, String realname, String email, int siteId) { user.username = name.getUsername(); user.setPassword(pass); @@ -413,8 +407,8 @@ public class UserRepository extends JORARepository } /** - * Inserts the supplied user record into the user database, assigning - * it a userid in the process, which is returned. + * Inserts the supplied user record into the user database, assigning it a userid in the + * process, which is returned. */ protected int insertUser (final User user) throws UserExistsException, PersistenceException @@ -444,8 +438,8 @@ public class UserRepository extends JORARepository } /** - * Loads up a user record that matches the specified where clause. Returns - * null if no record matches. + * Loads up a user record that matches the specified where clause. Returns null if no record + * matches. */ protected User loadUserWhere (String where) throws PersistenceException @@ -474,8 +468,7 @@ public class UserRepository extends JORARepository { Statement stmt = conn.createStatement(); try { - String query = "select userId, " + column + - " from users " + + String query = "select userId, " + column + " from users " + "where userId in (" + ids + ")"; ResultSet rs = stmt.executeQuery(query); while (rs.next()) { @@ -502,9 +495,8 @@ public class UserRepository extends JORARepository } /** - * Take the passed in int array and create the a string suitable for using - * in a SQL set query (I.e., "select foo, from bar where userid in - * (genIdString(userIds))"; ) + * Take the passed in int array and create the a string suitable for using in a SQL set query + * (I.e., "select foo, from bar where userid in (genIdString(userIds))"; ) */ protected String genIdString (int[] userIds) {