Skip to content

Commit 9b8c42e

Browse files
committed
Migrate JdbcTokenRepositoryImpl to JdbcPersistentTokenRepository
Since `JdbcDaoSupport` has been deprecated, we should deprecate our implementation of `JdbcTokenRepositoryImpl`, which directly inherits from `JdbcDaoSupport`. Instead of using `JdbcDaoSupport`, we are advised to inject `JdbcTemplate` or `JdbcClient` into the field, so we should create a new implementation of `JdbcPersistentTokenRepository`. References: gh-18982 Closes: gh-18987, gh-18986 Signed-off-by: Andrey Litvitski <andrey1010102008@gmail.com>
1 parent 8c4c5fe commit 9b8c42e

4 files changed

Lines changed: 357 additions & 0 deletions

File tree

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/*
2+
* Copyright 2004-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.web.authentication.rememberme;
18+
19+
import java.sql.ResultSet;
20+
import java.sql.SQLException;
21+
import java.util.Date;
22+
23+
import javax.sql.DataSource;
24+
25+
import org.apache.commons.logging.Log;
26+
import org.apache.commons.logging.LogFactory;
27+
import org.jspecify.annotations.Nullable;
28+
29+
import org.springframework.core.log.LogMessage;
30+
import org.springframework.dao.DataAccessException;
31+
import org.springframework.dao.EmptyResultDataAccessException;
32+
import org.springframework.dao.IncorrectResultSizeDataAccessException;
33+
import org.springframework.jdbc.core.JdbcTemplate;
34+
import org.springframework.jdbc.core.simple.JdbcClient;
35+
36+
/**
37+
* JDBC based persistent login token repository implementation.
38+
*
39+
* @author Andrey Litvitski
40+
* @since 7.1.0
41+
*/
42+
public class JdbcPersistentTokenRepository implements PersistentTokenRepository {
43+
44+
/** Default SQL for creating the database table to store the tokens */
45+
public static final String CREATE_TABLE_SQL = "create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, "
46+
+ "token varchar(64) not null, last_used timestamp not null)";
47+
48+
/** The default SQL used by the <tt>getTokenBySeries</tt> query */
49+
public static final String DEF_TOKEN_BY_SERIES_SQL = "select username,series,token,last_used from persistent_logins where series = ?";
50+
51+
/** The default SQL used by <tt>createNewToken</tt> */
52+
public static final String DEF_INSERT_TOKEN_SQL = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";
53+
54+
/** The default SQL used by <tt>updateToken</tt> */
55+
public static final String DEF_UPDATE_TOKEN_SQL = "update persistent_logins set token = ?, last_used = ? where series = ?";
56+
57+
/** The default SQL used by <tt>removeUserTokens</tt> */
58+
public static final String DEF_REMOVE_USER_TOKENS_SQL = "delete from persistent_logins where username = ?";
59+
60+
private String tokensBySeriesSql = DEF_TOKEN_BY_SERIES_SQL;
61+
62+
private String insertTokenSql = DEF_INSERT_TOKEN_SQL;
63+
64+
private String updateTokenSql = DEF_UPDATE_TOKEN_SQL;
65+
66+
private String removeUserTokensSql = DEF_REMOVE_USER_TOKENS_SQL;
67+
68+
private boolean createTableOnStartup;
69+
70+
private final JdbcClient jdbcClient;
71+
72+
protected final Log logger = LogFactory.getLog(this.getClass());
73+
74+
public JdbcPersistentTokenRepository(JdbcClient jdbcClient) {
75+
this.jdbcClient = jdbcClient;
76+
}
77+
78+
public JdbcPersistentTokenRepository(DataSource dataSource) {
79+
this.jdbcClient = JdbcClient.create(dataSource);
80+
}
81+
82+
public JdbcPersistentTokenRepository(JdbcTemplate jdbcTemplate) {
83+
this.jdbcClient = JdbcClient.create(jdbcTemplate);
84+
}
85+
86+
@Override
87+
public void createNewToken(PersistentRememberMeToken token) {
88+
this.jdbcClient.sql(this.insertTokenSql)
89+
.param(token.getUsername())
90+
.param(token.getSeries())
91+
.param(token.getTokenValue())
92+
.param(token.getDate())
93+
.update();
94+
}
95+
96+
@Override
97+
public void updateToken(String series, String tokenValue, Date lastUsed) {
98+
this.jdbcClient.sql(this.updateTokenSql).param(tokenValue).param(lastUsed).param(series).update();
99+
}
100+
101+
/**
102+
* Loads the token data for the supplied series identifier. If an error occurs, it
103+
* will be reported and null will be returned (since the result should just be a
104+
* failed persistent login).
105+
* @param seriesId
106+
* @return the token matching the series, or null if no match found or an exception
107+
* occurred.
108+
*/
109+
@Override
110+
public @Nullable PersistentRememberMeToken getTokenForSeries(String seriesId) {
111+
try {
112+
return this.jdbcClient.sql(this.tokensBySeriesSql)
113+
.param(seriesId)
114+
.query(this::createRememberMeToken)
115+
.single();
116+
}
117+
catch (EmptyResultDataAccessException ex) {
118+
this.logger.debug(LogMessage.format("Querying token for series '%s' returned no results.", seriesId), ex);
119+
}
120+
catch (IncorrectResultSizeDataAccessException ex) {
121+
this.logger.error(LogMessage.format(
122+
"Querying token for series '%s' returned more than one value. Series" + " should be unique",
123+
seriesId));
124+
}
125+
catch (DataAccessException ex) {
126+
this.logger.error("Failed to load token for series " + seriesId, ex);
127+
}
128+
return null;
129+
}
130+
131+
private PersistentRememberMeToken createRememberMeToken(ResultSet rs, int rowNum) throws SQLException {
132+
return new PersistentRememberMeToken(rs.getString(1), rs.getString(2), rs.getString(3), rs.getTimestamp(4));
133+
}
134+
135+
@Override
136+
public void removeUserTokens(String username) {
137+
this.jdbcClient.sql(this.removeUserTokensSql).param(username).update();
138+
}
139+
140+
/**
141+
* Intended for convenience in debugging. Will create the persistent_tokens database
142+
* table when the class is initialized during the initDao method.
143+
* @param createTableOnStartup set to true to execute the
144+
*/
145+
public void setCreateTableOnStartup(boolean createTableOnStartup) {
146+
this.createTableOnStartup = createTableOnStartup;
147+
}
148+
149+
protected void initDao() {
150+
if (this.createTableOnStartup) {
151+
this.jdbcClient.sql(CREATE_TABLE_SQL).update();
152+
}
153+
}
154+
155+
}

web/src/main/java/org/springframework/security/web/authentication/rememberme/JdbcTokenRepositoryImpl.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@
3434
*
3535
* @author Luke Taylor
3636
* @since 2.0
37+
* @deprecated Use {@link JdbcPersistentTokenRepository}
3738
*/
39+
@Deprecated(since = "7.1.0")
3840
@SuppressWarnings("removal")
3941
public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implements PersistentTokenRepository {
4042

web/src/main/java/org/springframework/security/web/authentication/rememberme/PersistentTokenRepository.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
* @since 2.0
2929
* @see JdbcTokenRepositoryImpl
3030
* @see InMemoryTokenRepositoryImpl
31+
* @see JdbcPersistentTokenRepository
3132
*/
3233
public interface PersistentTokenRepository {
3334

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/*
2+
* Copyright 2004-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.web.authentication.rememberme;
18+
19+
import java.sql.Timestamp;
20+
import java.util.Calendar;
21+
import java.util.Date;
22+
import java.util.List;
23+
import java.util.Map;
24+
25+
import org.apache.commons.logging.Log;
26+
import org.junit.jupiter.api.AfterAll;
27+
import org.junit.jupiter.api.AfterEach;
28+
import org.junit.jupiter.api.BeforeAll;
29+
import org.junit.jupiter.api.BeforeEach;
30+
import org.junit.jupiter.api.Test;
31+
import org.junit.jupiter.api.extension.ExtendWith;
32+
import org.mockito.ArgumentCaptor;
33+
import org.mockito.Mock;
34+
import org.mockito.junit.jupiter.MockitoExtension;
35+
36+
import org.springframework.dao.EmptyResultDataAccessException;
37+
import org.springframework.jdbc.core.simple.JdbcClient;
38+
import org.springframework.jdbc.datasource.SingleConnectionDataSource;
39+
import org.springframework.test.util.ReflectionTestUtils;
40+
41+
import static org.assertj.core.api.Assertions.assertThat;
42+
import static org.mockito.ArgumentMatchers.any;
43+
import static org.mockito.ArgumentMatchers.anyString;
44+
import static org.mockito.ArgumentMatchers.eq;
45+
import static org.mockito.BDDMockito.given;
46+
import static org.mockito.BDDMockito.mock;
47+
import static org.mockito.BDDMockito.then;
48+
49+
/**
50+
* @author Andrey Litvitski
51+
*/
52+
@ExtendWith(MockitoExtension.class)
53+
public class JdbcPersistentTokenRepositoryTests {
54+
55+
@Mock
56+
private Log logger;
57+
58+
private static SingleConnectionDataSource dataSource;
59+
60+
private JdbcPersistentTokenRepository repo;
61+
62+
private JdbcClient client;
63+
64+
@BeforeAll
65+
public static void createDataSource() {
66+
dataSource = new SingleConnectionDataSource("jdbc:hsqldb:mem:tokenrepotest", "sa", "", true);
67+
dataSource.setDriverClassName("org.hsqldb.jdbc.JDBCDriver");
68+
}
69+
70+
@AfterAll
71+
public static void clearDataSource() {
72+
dataSource.destroy();
73+
dataSource = null;
74+
}
75+
76+
@BeforeEach
77+
public void populateDatabase() {
78+
this.client = JdbcClient.create(dataSource);
79+
this.client
80+
.sql("create table persistent_logins (username varchar(100) not null, "
81+
+ "series varchar(100) not null, token varchar(500) not null, last_used timestamp not null)")
82+
.update();
83+
this.repo = new JdbcPersistentTokenRepository(this.client);
84+
ReflectionTestUtils.setField(this.repo, "logger", this.logger);
85+
this.repo.initDao();
86+
}
87+
88+
@AfterEach
89+
public void clearData() {
90+
this.client.sql("drop table persistent_logins").update();
91+
}
92+
93+
@Test
94+
public void createNewTokenInsertsCorrectData() {
95+
Timestamp currentDate = new Timestamp(Calendar.getInstance().getTimeInMillis());
96+
PersistentRememberMeToken token = new PersistentRememberMeToken("joeuser", "joesseries", "atoken", currentDate);
97+
this.repo.createNewToken(token);
98+
Map<String, Object> results = this.client.sql("select * from persistent_logins").query().singleRow();
99+
assertThat(results).containsEntry("last_used", currentDate);
100+
assertThat(results).containsEntry("username", "joeuser");
101+
assertThat(results).containsEntry("series", "joesseries");
102+
assertThat(results).containsEntry("token", "atoken");
103+
}
104+
105+
@Test
106+
public void retrievingTokenReturnsCorrectData() {
107+
this.client
108+
.sql("insert into persistent_logins (series, username, token, last_used) values "
109+
+ "('joesseries', 'joeuser', 'atoken', '2007-10-09 18:19:25.000000000')")
110+
.update();
111+
PersistentRememberMeToken token = this.repo.getTokenForSeries("joesseries");
112+
assertThat(token.getUsername()).isEqualTo("joeuser");
113+
assertThat(token.getSeries()).isEqualTo("joesseries");
114+
assertThat(token.getTokenValue()).isEqualTo("atoken");
115+
assertThat(token.getDate()).isEqualTo(Timestamp.valueOf("2007-10-09 18:19:25.000000000"));
116+
}
117+
118+
@Test
119+
public void retrievingTokenWithDuplicateSeriesReturnsNull() {
120+
this.client
121+
.sql("insert into persistent_logins (series, username, token, last_used) values "
122+
+ "('joesseries', 'joeuser', 'atoken2', '2007-10-19 18:19:25.000000000')")
123+
.update();
124+
this.client
125+
.sql("insert into persistent_logins (series, username, token, last_used) values "
126+
+ "('joesseries', 'joeuser', 'atoken', '2007-10-09 18:19:25.000000000')")
127+
.update();
128+
assertThat(this.repo.getTokenForSeries("joesseries")).isNull();
129+
}
130+
131+
// SEC-1964
132+
@Test
133+
public void retrievingTokenWithNoSeriesReturnsNull() {
134+
assertThat(this.repo.getTokenForSeries("missingSeries")).isNull();
135+
ArgumentCaptor<Object> captor = ArgumentCaptor.forClass(Object.class);
136+
then(this.logger).should().debug(captor.capture(), any(EmptyResultDataAccessException.class));
137+
then(this.logger).shouldHaveNoMoreInteractions();
138+
assertThat(captor.getValue()).hasToString("Querying token for series 'missingSeries' returned no results.");
139+
}
140+
141+
@Test
142+
public void removingUserTokensDeletesData() {
143+
this.client
144+
.sql("insert into persistent_logins (series, username, token, last_used) values "
145+
+ "('joesseries2', 'joeuser', 'atoken2', '2007-10-19 18:19:25.000000000')")
146+
.update();
147+
this.client
148+
.sql("insert into persistent_logins (series, username, token, last_used) values "
149+
+ "('joesseries', 'joeuser', 'atoken', '2007-10-09 18:19:25.000000000')")
150+
.update();
151+
this.repo.removeUserTokens("joeuser");
152+
List<Map<String, Object>> results = this.client
153+
.sql("select * from persistent_logins where username = 'joeuser'")
154+
.query()
155+
.listOfRows();
156+
assertThat(results).isEmpty();
157+
}
158+
159+
@Test
160+
public void updatingTokenModifiesTokenValueAndLastUsed() {
161+
Timestamp ts = new Timestamp(System.currentTimeMillis() - 1);
162+
this.client
163+
.sql("insert into persistent_logins (series, username, token, last_used) values "
164+
+ "('joesseries', 'joeuser', 'atoken', '" + ts + "')")
165+
.update();
166+
this.repo.updateToken("joesseries", "newtoken", new Date());
167+
Map<String, Object> results = this.client.sql("select * from persistent_logins where series = 'joesseries'")
168+
.query()
169+
.singleRow();
170+
assertThat(results).containsEntry("username", "joeuser");
171+
assertThat(results).containsEntry("series", "joesseries");
172+
assertThat(results).containsEntry("token", "newtoken");
173+
Date lastUsed = (Date) results.get("last_used");
174+
assertThat(lastUsed.getTime() > ts.getTime()).isTrue();
175+
}
176+
177+
@Test
178+
public void createTableOnStartupCreatesCorrectTable() {
179+
this.client.sql("drop table persistent_logins").update();
180+
this.repo = new JdbcPersistentTokenRepository(this.client);
181+
this.repo.setCreateTableOnStartup(true);
182+
this.repo.initDao();
183+
this.client.sql("select username,series,token,last_used from persistent_logins").query().listOfRows();
184+
}
185+
186+
// SEC-2879
187+
@Test
188+
public void updateUsesLastUsed() {
189+
JdbcClient mockClient = mock(JdbcClient.class);
190+
JdbcClient.StatementSpec statementSpec = mock(JdbcClient.StatementSpec.class);
191+
Date lastUsed = new Date(1424841314059L);
192+
given(mockClient.sql(anyString())).willReturn(statementSpec);
193+
given(statementSpec.param(any())).willReturn(statementSpec);
194+
JdbcPersistentTokenRepository repository = new JdbcPersistentTokenRepository(mockClient);
195+
repository.updateToken("series", "token", lastUsed);
196+
then(statementSpec).should().param(eq(lastUsed));
197+
}
198+
199+
}

0 commit comments

Comments
 (0)