diff --git a/core/src/main/java/org/springframework/security/ui/rememberme/AbstractRememberMeServices.java b/core/src/main/java/org/springframework/security/ui/rememberme/AbstractRememberMeServices.java index 946744dd16..6b94763c66 100644 --- a/core/src/main/java/org/springframework/security/ui/rememberme/AbstractRememberMeServices.java +++ b/core/src/main/java/org/springframework/security/ui/rememberme/AbstractRememberMeServices.java @@ -48,10 +48,10 @@ public abstract class AbstractRememberMeServices implements RememberMeServices, private int tokenValiditySeconds = 1209600; // 14 days public void afterPropertiesSet() throws Exception { - Assert.hasLength(key); + Assert.hasLength(key); Assert.hasLength(parameter); Assert.hasLength(cookieName); - Assert.notNull(userDetailsService); + Assert.notNull(userDetailsService); } /** @@ -81,7 +81,7 @@ public abstract class AbstractRememberMeServices implements RememberMeServices, cancelCookie(request, response); throw cte; } catch (UsernameNotFoundException noUser) { - cancelCookie(request, response); + cancelCookie(request, response); logger.debug("Remember-me login was valid but corresponding user not found.", noUser); return null; } catch (InvalidCookieException invalidCookie) { @@ -90,7 +90,7 @@ public abstract class AbstractRememberMeServices implements RememberMeServices, return null; } catch (RememberMeAuthenticationException e) { cancelCookie(request, response); - logger.debug("autoLogin failed", e); + logger.debug(e.getMessage()); return null; } @@ -286,7 +286,7 @@ public abstract class AbstractRememberMeServices implements RememberMeServices, } cancelCookie(request, response); } - + public void setCookieName(String cookieName) { this.cookieName = cookieName; } @@ -322,7 +322,7 @@ public abstract class AbstractRememberMeServices implements RememberMeServices, public int getTokenValiditySeconds() { return tokenValiditySeconds; } - + public AuthenticationDetailsSource getAuthenticationDetailsSource() { return authenticationDetailsSource; } diff --git a/core/src/main/java/org/springframework/security/ui/rememberme/JdbcTokenRepositoryImpl.java b/core/src/main/java/org/springframework/security/ui/rememberme/JdbcTokenRepositoryImpl.java index 69b868f55f..a80fc5e01a 100644 --- a/core/src/main/java/org/springframework/security/ui/rememberme/JdbcTokenRepositoryImpl.java +++ b/core/src/main/java/org/springframework/security/ui/rememberme/JdbcTokenRepositoryImpl.java @@ -1,34 +1,159 @@ package org.springframework.security.ui.rememberme; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.jdbc.core.SqlParameter; import org.springframework.jdbc.core.support.JdbcDaoSupport; +import org.springframework.jdbc.object.MappingSqlQuery; +import org.springframework.jdbc.object.SqlUpdate; + +import javax.sql.DataSource; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Date; /** - * + * JDBC based persistent login token repository implementation. + * * @author Luke Taylor * @version $Id$ */ public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implements PersistentTokenRepository { - //~ Static fields/initializers ===================================================================================== - public static final String DEF_TOKEN_BY_SERIES_QUERY = - "select username,series,token from persistent_logins where series = ?"; - public static final String DEF_INSERT_TOKEN_STATEMENT = - "insert into persistent_logins (username,series,token) values(?,?,?)"; - public static final String DEF_REMOVE_USER_TOKENS_STATEMENT = + //~ Static fields/initializers ===================================================================================== + + /** Default SQL for creating the database table to store the tokens */ + public static final String CREATE_TABLE_SQL = + "create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, " + + "token varchar(64) not null, last_used timestamp not null)"; + /** The default SQL used by the getTokenBySeries query */ + public static final String DEF_TOKEN_BY_SERIES_SQL = + "select username,series,token,last_used from persistent_logins where series = ?"; + /** The default SQL used by createNewToken */ + public static final String DEF_INSERT_TOKEN_SQL = + "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)"; + /** The default SQL used by updateToken */ + public static final String DEF_UPDATE_TOKEN_SQL = + "update persistent_logins set token = ?, last_used = ? where series = ?"; + /** The default SQL used by removeUserTokens */ + public static final String DEF_REMOVE_USER_TOKENS_SQL = "delete from persistent_logins where username = ?"; //~ Instance fields ================================================================================================ - private String tokensBySeriesQuery = DEF_TOKEN_BY_SERIES_QUERY; - private String insertTokenStatement = DEF_INSERT_TOKEN_STATEMENT; - private String removeUserTokensStatement = DEF_REMOVE_USER_TOKENS_STATEMENT; + private String tokensBySeriesSql = DEF_TOKEN_BY_SERIES_SQL; + private String insertTokenSql = DEF_INSERT_TOKEN_SQL; + private String updateTokenSql = DEF_UPDATE_TOKEN_SQL; + private String removeUserTokensSql = DEF_REMOVE_USER_TOKENS_SQL; + private boolean createTableOnStartup; - public void saveToken(PersistentRememberMeToken token) { + protected MappingSqlQuery tokensBySeriesMapping; + protected SqlUpdate insertToken; + protected SqlUpdate updateToken; + protected SqlUpdate removeUserTokens; + + protected void initDao() { + tokensBySeriesMapping = new TokensBySeriesMapping(getDataSource()); + insertToken = new InsertToken(getDataSource()); + updateToken = new UpdateToken(getDataSource()); + removeUserTokens = new RemoveUserTokens(getDataSource()); + + if (createTableOnStartup) { + getJdbcTemplate().execute(CREATE_TABLE_SQL); + } } + public void createNewToken(PersistentRememberMeToken token) { + insertToken.update( + new Object[] {token.getUsername(), token.getSeries(), token.getTokenValue(), token.getDate()}); + } + + public void updateToken(String series, String tokenValue, Date lastUsed) { + updateToken.update(new Object[] {tokenValue, new Date(), series}); + } + + /** + * Loads the token data for the supplied series identifier. + * + * If an error occurs, it will be reported and null will be returned (since the result should just be a failed + * persistent login). + * + * @param seriesId + * @return the token matching the series, or null if no match found or an exception occurred. + */ public PersistentRememberMeToken getTokenForSeries(String seriesId) { + try { + return (PersistentRememberMeToken) tokensBySeriesMapping.findObject(seriesId); + } catch(IncorrectResultSizeDataAccessException moreThanOne) { + logger.error("Querying token for series '" + seriesId + "' returned more than one value. Series" + + "should be unique"); + } catch(DataAccessException e) { + logger.error("Failed to load token for series " + seriesId, e); + } + return null; } - public void removeAllTokens(String username) { + public void removeUserTokens(String username) { + removeUserTokens.update(username); + } + + /** + * Intended for convenience in debugging. Will create the persistent_tokens database table when the class + * is initialized during the initDao method. + * + * @param createTableOnStartup set to true to execute the + */ + public void setCreateTableOnStartup(boolean createTableOnStartup) { + this.createTableOnStartup = createTableOnStartup; + } + + //~ Inner Classes ================================================================================================== + + protected class TokensBySeriesMapping extends MappingSqlQuery { + protected TokensBySeriesMapping(DataSource ds) { + super(ds, tokensBySeriesSql); + declareParameter(new SqlParameter(Types.VARCHAR)); + compile(); + } + + protected Object mapRow(ResultSet rs, int rowNum) throws SQLException { + PersistentRememberMeToken token = + new PersistentRememberMeToken(rs.getString(1), rs.getString(2), rs.getString(3), rs.getTimestamp(4)); + + return token; + } + } + + protected class UpdateToken extends SqlUpdate { + + public UpdateToken(DataSource ds) { + super(ds, updateTokenSql); + setMaxRowsAffected(1); + declareParameter(new SqlParameter(Types.VARCHAR)); + declareParameter(new SqlParameter(Types.TIMESTAMP)); + declareParameter(new SqlParameter(Types.VARCHAR)); + compile(); + } + } + + protected class InsertToken extends SqlUpdate { + + public InsertToken(DataSource ds) { + super(ds, insertTokenSql); + declareParameter(new SqlParameter(Types.VARCHAR)); + declareParameter(new SqlParameter(Types.VARCHAR)); + declareParameter(new SqlParameter(Types.VARCHAR)); + declareParameter(new SqlParameter(Types.TIMESTAMP)); + compile(); + } + } + + protected class RemoveUserTokens extends SqlUpdate { + public RemoveUserTokens(DataSource ds) { + super(ds, removeUserTokensSql); + declareParameter(new SqlParameter(Types.VARCHAR)); + compile(); + } } } diff --git a/core/src/main/java/org/springframework/security/ui/rememberme/PersistentTokenBasedRememberMeServices.java b/core/src/main/java/org/springframework/security/ui/rememberme/PersistentTokenBasedRememberMeServices.java index 894ddb44ce..5a9e3c195d 100644 --- a/core/src/main/java/org/springframework/security/ui/rememberme/PersistentTokenBasedRememberMeServices.java +++ b/core/src/main/java/org/springframework/security/ui/rememberme/PersistentTokenBasedRememberMeServices.java @@ -1,6 +1,7 @@ package org.springframework.security.ui.rememberme; import org.apache.commons.codec.binary.Base64; +import org.springframework.dao.DataAccessException; import org.springframework.security.Authentication; import javax.servlet.http.HttpServletRequest; @@ -10,7 +11,7 @@ import java.util.Arrays; import java.util.Date; /** - * {@link RememberMeServices} implementation based on Barry Jaspan's + * {@link RememberMeServices} implementation based on Barry Jaspan's * Improved Persistent Login Cookie * Best Practice. * @@ -80,7 +81,7 @@ public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeSe // We have a match for this user/series combination if (!presentedToken.equals(token.getTokenValue())) { // Token doesn't match series value. Delete all logins for this user and throw an exception to warn them. - tokenRepository.removeAllTokens(token.getUsername()); + tokenRepository.removeUserTokens(token.getUsername()); throw new CookieTheftException(messages.getMessage("PersistentTokenBasedRememberMeServices.cookieStolen", "Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack.")); @@ -90,10 +91,22 @@ public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeSe throw new RememberMeAuthenticationException("Remember-me login has expired"); } - // Token also matches, so login is valid. create and save new token with the *same* series number. - PersistentRememberMeToken newToken = createNewToken(token.getUsername(), token.getSeries()); + // Token also matches, so login is valid. Update the token value, keeping the *same* series number. + if (logger.isDebugEnabled()) { + logger.debug("Refreshing persistent login token for user '" + token.getUsername() + "', series '" + + token.getSeries() + "'"); + } - addCookie(newToken, request, response); + PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), + token.getSeries(), generateTokenData(), new Date()); + + try { + tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate()); + addCookie(newToken, request, response); + } catch (DataAccessException e) { + logger.error("Failed to update token: ", e); + throw new RememberMeAuthenticationException("Autologin failed due to data access problem"); + } return token.getUsername(); } @@ -104,32 +117,31 @@ public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeSe * */ protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { - PersistentRememberMeToken token = createNewToken(successfulAuthentication.getName(), null); - addCookie(token, request, response); + String username = successfulAuthentication.getName(); + + logger.debug("Creating new persistent login for user " + username); + + PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, generateSeriesData(), + generateTokenData(), new Date()); + try { + tokenRepository.createNewToken(persistentToken); + addCookie(persistentToken, request, response); + } catch (DataAccessException e) { + logger.error("Failed to save persistent token ", e); + + } } - private PersistentRememberMeToken createNewToken(String username, String series) { - if (logger.isDebugEnabled()) { - logger.debug(series == null ? "Creating new" : "Renewing" + - " persistent login token for user " + username); - } + protected String generateSeriesData() { + byte[] newSeries = new byte[seriesLength]; + random.nextBytes(newSeries); + return new String(Base64.encodeBase64(newSeries)); + } - if (series == null) { - byte[] newSeries = new byte[seriesLength]; - random.nextBytes(newSeries); - series = new String(Base64.encodeBase64(newSeries)); - logger.debug("New series: " + series); - } - - byte[] token = new byte[tokenLength]; - random.nextBytes(token); - - PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, series, - new String(Base64.encodeBase64(token)), new Date()); - - tokenRepository.saveToken(persistentToken); - - return persistentToken; + protected String generateTokenData() { + byte[] newToken = new byte[tokenLength]; + random.nextBytes(newToken); + return new String(Base64.encodeBase64(newToken)); } private void addCookie(PersistentRememberMeToken token, HttpServletRequest request, HttpServletResponse response) { diff --git a/core/src/main/java/org/springframework/security/ui/rememberme/PersistentTokenRepository.java b/core/src/main/java/org/springframework/security/ui/rememberme/PersistentTokenRepository.java index 0e21918411..f5e051a347 100644 --- a/core/src/main/java/org/springframework/security/ui/rememberme/PersistentTokenRepository.java +++ b/core/src/main/java/org/springframework/security/ui/rememberme/PersistentTokenRepository.java @@ -1,15 +1,25 @@ package org.springframework.security.ui.rememberme; +import java.util.Date; + /** + * The abstraction used by {@link PersistentTokenBasedRememberMeServices} to store the persistent + * login tokens for a user. + * + * @see JdbcTokenRepositoryImpl + * @see InMemoryTokenRepositoryImpl + * * @author Luke Taylor * @version $Id$ */ public interface PersistentTokenRepository { - void saveToken(PersistentRememberMeToken token); + void createNewToken(PersistentRememberMeToken token); + + void updateToken(String series, String tokenValue, Date lastUsed); PersistentRememberMeToken getTokenForSeries(String seriesId); - void removeAllTokens(String username); + void removeUserTokens(String username); } diff --git a/core/src/test/java/org/springframework/security/ui/rememberme/JdbcTokenRepositoryImplTests.java b/core/src/test/java/org/springframework/security/ui/rememberme/JdbcTokenRepositoryImplTests.java new file mode 100644 index 0000000000..50885a0f66 --- /dev/null +++ b/core/src/test/java/org/springframework/security/ui/rememberme/JdbcTokenRepositoryImplTests.java @@ -0,0 +1,132 @@ +package org.springframework.security.ui.rememberme; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DriverManagerDataSource; + +import org.junit.After; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.sql.Timestamp; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * @author Luke Taylor + * @version $Id$ + */ +public class JdbcTokenRepositoryImplTests { + private static DriverManagerDataSource dataSource; + private JdbcTokenRepositoryImpl repo; + private JdbcTemplate template; + + @BeforeClass + public static void createDataSource() { + dataSource = new DriverManagerDataSource(); + dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); + dataSource.setUrl("jdbc:hsqldb:mem:tokenrepotest"); + dataSource.setUsername("sa"); + dataSource.setPassword(""); + } + + @Before + public void populateDatabase() { + repo = new JdbcTokenRepositoryImpl(); + repo.setDataSource(dataSource); + repo.initDao(); + template = repo.getJdbcTemplate(); + template.execute("create table persistent_logins (username varchar not null, " + + "series varchar not null, token varchar not null, last_used timestamp not null)"); + } + + @After + public void clearData() { + template.execute("drop table persistent_logins"); + } + + @Test + public void createNewTokenInsertsCorrectData() { + Date currentDate = new Date(); + PersistentRememberMeToken token = new PersistentRememberMeToken("joeuser", "joesseries", "atoken", currentDate); + repo.createNewToken(token); + + Map results = template.queryForMap("select * from persistent_logins"); + + assertEquals(currentDate, results.get("last_used")); + assertEquals("joeuser", results.get("username")); + assertEquals("joesseries", results.get("series")); + assertEquals("atoken", results.get("token")); + } + + @Test + public void retrievingTokenReturnsCorrectData() { + + template.execute("insert into persistent_logins (series, username, token, last_used) values " + + "('joesseries', 'joeuser', 'atoken', '2007-10-09 18:19:25.000000000')"); + PersistentRememberMeToken token = repo.getTokenForSeries("joesseries"); + + assertEquals("joeuser", token.getUsername()); + assertEquals("joesseries", token.getSeries()); + assertEquals("atoken", token.getTokenValue()); + assertEquals(Timestamp.valueOf("2007-10-09 18:19:25.000000000"), token.getDate()); + } + + @Test + public void retrievingTokenWithDuplicateSeriesReturnsNull() { + template.execute("insert into persistent_logins (series, username, token, last_used) values " + + "('joesseries', 'joeuser', 'atoken2', '2007-10-19 18:19:25.000000000')"); + template.execute("insert into persistent_logins (series, username, token, last_used) values " + + "('joesseries', 'joeuser', 'atoken', '2007-10-09 18:19:25.000000000')"); + +// List results = template.queryForList("select * from persistent_logins where series = 'joesseries'"); + + assertNull(repo.getTokenForSeries("joesseries")); + } + + @Test + public void removingUserTokensDeletesData() { + template.execute("insert into persistent_logins (series, username, token, last_used) values " + + "('joesseries2', 'joeuser', 'atoken2', '2007-10-19 18:19:25.000000000')"); + template.execute("insert into persistent_logins (series, username, token, last_used) values " + + "('joesseries', 'joeuser', 'atoken', '2007-10-09 18:19:25.000000000')"); + + // List results = template.queryForList("select * from persistent_logins where series = 'joesseries'"); + + repo.removeUserTokens("joeuser"); + + List results = template.queryForList("select * from persistent_logins where username = 'joeuser'"); + + assertEquals(0, results.size()); + } + + @Test + public void updatingTokenModifiesTokenValueAndLastUsed() { + Timestamp ts = new Timestamp(System.currentTimeMillis() - 1); + template.execute("insert into persistent_logins (series, username, token, last_used) values " + + "('joesseries', 'joeuser', 'atoken', '" + ts.toString() + "')"); + repo.updateToken("joesseries", "newtoken", new Date()); + + Map results = template.queryForMap("select * from persistent_logins where series = 'joesseries'"); + + assertEquals("joeuser", results.get("username")); + assertEquals("joesseries", results.get("series")); + assertEquals("newtoken", results.get("token")); + Date lastUsed = (Date) results.get("last_used"); + assertTrue(lastUsed.getTime() > ts.getTime()); + } + + @Test + public void createTableOnStartupCreatesCorrectTable() { + template.execute("drop table persistent_logins"); + repo = new JdbcTokenRepositoryImpl(); + repo.setDataSource(dataSource); + repo.setCreateTableOnStartup(true); + repo.initDao(); + + template.queryForList("select username,series,token,last_used from persistent_logins"); + } + +} diff --git a/core/src/test/java/org/springframework/security/ui/rememberme/PersistentTokenBasedRememberMeServicesTests.java b/core/src/test/java/org/springframework/security/ui/rememberme/PersistentTokenBasedRememberMeServicesTests.java index 7a7c6e6138..0573bdaeb4 100644 --- a/core/src/test/java/org/springframework/security/ui/rememberme/PersistentTokenBasedRememberMeServicesTests.java +++ b/core/src/test/java/org/springframework/security/ui/rememberme/PersistentTokenBasedRememberMeServicesTests.java @@ -24,7 +24,7 @@ public class PersistentTokenBasedRememberMeServicesTests { @Test(expected = InvalidCookieException.class) public void loginIsRejectedWithWrongNumberOfCookieTokens() { - services.processAutoLoginCookie(new String[] {"series", "token", "extra"}, new MockHttpServletRequest(), + services.processAutoLoginCookie(new String[] {"series", "token", "extra"}, new MockHttpServletRequest(), new MockHttpServletResponse()); } @@ -101,15 +101,20 @@ public class PersistentTokenBasedRememberMeServicesTests { storedToken = token; } - public void saveToken(PersistentRememberMeToken token) { + public void createNewToken(PersistentRememberMeToken token) { storedToken = token; } + public void updateToken(String series, String tokenValue, Date lastUsed) { + storedToken = new PersistentRememberMeToken(storedToken.getUsername(), storedToken.getSeries(), + tokenValue, lastUsed); + } + public PersistentRememberMeToken getTokenForSeries(String seriesId) { return storedToken; } - public void removeAllTokens(String username) { + public void removeUserTokens(String username) { } PersistentRememberMeToken getStoredToken() {