Skip to content

Commit 079523a

Browse files
Add support JdbcOneTimeTokenService
Closes spring-projectsgh-15735
1 parent 2763bbe commit 079523a

File tree

10 files changed

+709
-30
lines changed

10 files changed

+709
-30
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2002-2024 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.aot.hint;
18+
19+
import org.springframework.aot.hint.RuntimeHints;
20+
import org.springframework.aot.hint.RuntimeHintsRegistrar;
21+
import org.springframework.jdbc.core.JdbcOperations;
22+
import org.springframework.security.authentication.ott.OneTimeToken;
23+
import org.springframework.security.authentication.ott.OneTimeTokenService;
24+
25+
/**
26+
*
27+
* A JDBC implementation of an {@link OneTimeTokenService} that uses a
28+
* {@link JdbcOperations} for {@link OneTimeToken} persistence.
29+
*
30+
* @author Max Batischev
31+
* @since 6.4
32+
*/
33+
class OneTimeTokenRuntimeHints implements RuntimeHintsRegistrar {
34+
35+
@Override
36+
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
37+
hints.resources().registerPattern("org/springframework/security/core/ott/jdbc/one-time-tokens.sql");
38+
}
39+
40+
}

core/src/main/java/org/springframework/security/authentication/ott/InMemoryOneTimeTokenService.java

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,11 @@
1616

1717
package org.springframework.security.authentication.ott;
1818

19-
import java.time.Clock;
20-
import java.time.Instant;
2119
import java.util.Map;
2220
import java.util.UUID;
2321
import java.util.concurrent.ConcurrentHashMap;
2422

2523
import org.springframework.lang.NonNull;
26-
import org.springframework.util.Assert;
2724

2825
/**
2926
* Provides an in-memory implementation of the {@link OneTimeTokenService} interface that
@@ -38,23 +35,20 @@ public final class InMemoryOneTimeTokenService implements OneTimeTokenService {
3835

3936
private final Map<String, OneTimeToken> oneTimeTokenByToken = new ConcurrentHashMap<>();
4037

41-
private Clock clock = Clock.systemUTC();
42-
4338
@Override
4439
@NonNull
4540
public OneTimeToken generate(GenerateOneTimeTokenRequest request) {
46-
String token = UUID.randomUUID().toString();
47-
Instant fiveMinutesFromNow = this.clock.instant().plusSeconds(300);
48-
OneTimeToken ott = new DefaultOneTimeToken(token, request.getUsername(), fiveMinutesFromNow);
49-
this.oneTimeTokenByToken.put(token, ott);
41+
OneTimeToken ott = OneTimeTokenUtils.generateOneTimeToken(request,
42+
OneTimeTokenUtils.DEFAULT_ONE_TIME_TOKEN_TIME_TO_LIVE);
43+
this.oneTimeTokenByToken.put(ott.getTokenValue(), ott);
5044
cleanExpiredTokensIfNeeded();
5145
return ott;
5246
}
5347

5448
@Override
5549
public OneTimeToken consume(OneTimeTokenAuthenticationToken authenticationToken) {
5650
OneTimeToken ott = this.oneTimeTokenByToken.remove(authenticationToken.getTokenValue());
57-
if (ott == null || isExpired(ott)) {
51+
if (ott == null || OneTimeTokenUtils.isExpired(ott)) {
5852
return null;
5953
}
6054
return ott;
@@ -65,19 +59,10 @@ private void cleanExpiredTokensIfNeeded() {
6559
return;
6660
}
6761
for (Map.Entry<String, OneTimeToken> entry : this.oneTimeTokenByToken.entrySet()) {
68-
if (isExpired(entry.getValue())) {
62+
if (OneTimeTokenUtils.isExpired(entry.getValue())) {
6963
this.oneTimeTokenByToken.remove(entry.getKey());
7064
}
7165
}
7266
}
7367

74-
private boolean isExpired(OneTimeToken ott) {
75-
return this.clock.instant().isAfter(ott.getExpiresAt());
76-
}
77-
78-
void setClock(Clock clock) {
79-
Assert.notNull(clock, "clock cannot be null");
80-
this.clock = clock;
81-
}
82-
8368
}
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
/*
2+
* Copyright 2002-2024 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.authentication.ott;
18+
19+
import java.sql.ResultSet;
20+
import java.sql.SQLException;
21+
import java.sql.Timestamp;
22+
import java.sql.Types;
23+
import java.time.Instant;
24+
import java.util.ArrayList;
25+
import java.util.List;
26+
import java.util.function.Function;
27+
28+
import org.springframework.jdbc.core.ArgumentPreparedStatementSetter;
29+
import org.springframework.jdbc.core.JdbcOperations;
30+
import org.springframework.jdbc.core.PreparedStatementSetter;
31+
import org.springframework.jdbc.core.RowMapper;
32+
import org.springframework.jdbc.core.SqlParameterValue;
33+
import org.springframework.util.Assert;
34+
import org.springframework.util.CollectionUtils;
35+
36+
/**
37+
*
38+
* A JDBC implementation of an {@link OneTimeTokenService} that uses a
39+
* {@link JdbcOperations} for {@link OneTimeToken} persistence.
40+
*
41+
* <p>
42+
* <b>NOTE:</b> This {@code JdbcOneTimeTokenService} depends on the table definition
43+
* described in "classpath:org/springframework/security/core/ott/jdbc/one-time-tokens.sql"
44+
* and therefore MUST be defined in the database schema.
45+
*
46+
* @author Max Batischev
47+
* @since 6.4
48+
*/
49+
public final class JdbcOneTimeTokenService implements OneTimeTokenService {
50+
51+
private final JdbcOperations jdbcOperations;
52+
53+
private Function<OneTimeToken, List<SqlParameterValue>> oneTimeTokenParametersMapper = new OneTimeTokenParametersMapper();
54+
55+
private RowMapper<OneTimeToken> oneTimeTokenRowMapper = new OneTimeTokenRowMapper();
56+
57+
private static final String TABLE_NAME = "one_time_tokens";
58+
59+
// @formatter:off
60+
private static final String COLUMN_NAMES = "token_value, "
61+
+ "username, "
62+
+ "expires_at";
63+
// @formatter:on
64+
65+
// @formatter:off
66+
private static final String SAVE_AUTHORIZED_CLIENT_SQL = "INSERT INTO " + TABLE_NAME
67+
+ " (" + COLUMN_NAMES + ") VALUES (?, ?, ?)";
68+
// @formatter:on
69+
70+
private static final String FILTER = "token_value = ?";
71+
72+
private static final String DELETE_ONE_TIME_TOKEN_SQL = "DELETE FROM " + TABLE_NAME + " WHERE " + FILTER;
73+
74+
// @formatter:off
75+
private static final String SELECT_ONE_TIME_TOKEN_SQL = "SELECT " + COLUMN_NAMES
76+
+ " FROM " + TABLE_NAME
77+
+ " WHERE " + FILTER;
78+
// @formatter:on
79+
80+
/**
81+
* Constructs a {@code JdbcOneTimeTokenService} using the provide parameters.
82+
* @param jdbcOperations the JDBC operations
83+
*/
84+
public JdbcOneTimeTokenService(JdbcOperations jdbcOperations) {
85+
Assert.notNull(jdbcOperations, "jdbcOperations cannot be null");
86+
this.jdbcOperations = jdbcOperations;
87+
}
88+
89+
@Override
90+
public OneTimeToken generate(GenerateOneTimeTokenRequest request) {
91+
Assert.notNull(request, "generateOneTimeTokenRequest cannot be null");
92+
93+
OneTimeToken oneTimeToken = OneTimeTokenUtils.generateOneTimeToken(request,
94+
OneTimeTokenUtils.DEFAULT_ONE_TIME_TOKEN_TIME_TO_LIVE);
95+
insertOneTimeToken(oneTimeToken);
96+
return oneTimeToken;
97+
}
98+
99+
private void insertOneTimeToken(OneTimeToken oneTimeToken) {
100+
List<SqlParameterValue> parameters = this.oneTimeTokenParametersMapper.apply(oneTimeToken);
101+
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
102+
this.jdbcOperations.update(SAVE_AUTHORIZED_CLIENT_SQL, pss);
103+
}
104+
105+
@Override
106+
public OneTimeToken consume(OneTimeTokenAuthenticationToken authenticationToken) {
107+
Assert.notNull(authenticationToken, "authenticationToken cannot be null");
108+
109+
List<OneTimeToken> tokens = selectOneTimeToken(authenticationToken);
110+
if (CollectionUtils.isEmpty(tokens)) {
111+
return null;
112+
}
113+
OneTimeToken token = tokens.get(0);
114+
deleteOneTimeToken(token);
115+
if (OneTimeTokenUtils.isExpired(token)) {
116+
return null;
117+
}
118+
return token;
119+
}
120+
121+
private List<OneTimeToken> selectOneTimeToken(OneTimeTokenAuthenticationToken authenticationToken) {
122+
List<SqlParameterValue> parameters = List
123+
.of(new SqlParameterValue(Types.VARCHAR, authenticationToken.getTokenValue()));
124+
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
125+
return this.jdbcOperations.query(SELECT_ONE_TIME_TOKEN_SQL, pss, this.oneTimeTokenRowMapper);
126+
}
127+
128+
private void deleteOneTimeToken(OneTimeToken oneTimeToken) {
129+
List<SqlParameterValue> parameters = List
130+
.of(new SqlParameterValue(Types.VARCHAR, oneTimeToken.getTokenValue()));
131+
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
132+
this.jdbcOperations.update(DELETE_ONE_TIME_TOKEN_SQL, pss);
133+
}
134+
135+
/**
136+
* Sets the {@code Function} used for mapping {@link OneTimeToken} to a {@code List}
137+
* of {@link SqlParameterValue}. The default is {@link OneTimeTokenParametersMapper}.
138+
* @param oneTimeTokenParametersMapper the {@code Function} used for mapping
139+
* {@link OneTimeToken} to a {@code List} of {@link SqlParameterValue}
140+
*/
141+
public void setOneTimeTokenParametersMapper(
142+
Function<OneTimeToken, List<SqlParameterValue>> oneTimeTokenParametersMapper) {
143+
Assert.notNull(oneTimeTokenParametersMapper, "oneTimeTokenParametersMapper cannot be null");
144+
this.oneTimeTokenParametersMapper = oneTimeTokenParametersMapper;
145+
}
146+
147+
/**
148+
* Sets the {@link RowMapper} used for mapping the current row in
149+
* {@code java.sql.ResultSet} to {@link OneTimeToken}. The default is
150+
* {@link OneTimeTokenRowMapper}.
151+
* @param oneTimeTokenRowMapper the {@link RowMapper} used for mapping the current row
152+
* in {@code java.sql.ResultSet} to {@link OneTimeToken}
153+
*/
154+
public void setOneTimeTokenRowMapper(RowMapper<OneTimeToken> oneTimeTokenRowMapper) {
155+
Assert.notNull(oneTimeTokenRowMapper, "oneTimeTokenRowMapper cannot be null");
156+
this.oneTimeTokenRowMapper = oneTimeTokenRowMapper;
157+
}
158+
159+
/**
160+
* The default {@code Function} that maps {@link OneTimeToken} to a {@code List} of
161+
* {@link SqlParameterValue}.
162+
*
163+
* @author Max Batischev
164+
* @since 6.4
165+
*/
166+
public static class OneTimeTokenParametersMapper implements Function<OneTimeToken, List<SqlParameterValue>> {
167+
168+
@Override
169+
public List<SqlParameterValue> apply(OneTimeToken oneTimeToken) {
170+
List<SqlParameterValue> parameters = new ArrayList<>();
171+
parameters.add(new SqlParameterValue(Types.VARCHAR, oneTimeToken.getTokenValue()));
172+
parameters.add(new SqlParameterValue(Types.VARCHAR, oneTimeToken.getUsername()));
173+
parameters.add(new SqlParameterValue(Types.TIMESTAMP, Timestamp.from(oneTimeToken.getExpiresAt())));
174+
return parameters;
175+
}
176+
177+
}
178+
179+
/**
180+
* The default {@link RowMapper} that maps the current row in
181+
* {@code java.sql.ResultSet} to {@link OneTimeToken}.
182+
*
183+
* @author Max Batischev
184+
* @since 6.4
185+
*/
186+
public static class OneTimeTokenRowMapper implements RowMapper<OneTimeToken> {
187+
188+
@Override
189+
public OneTimeToken mapRow(ResultSet rs, int rowNum) throws SQLException {
190+
String tokenValue = rs.getString("token_value");
191+
String userName = rs.getString("username");
192+
Instant expiresAt = rs.getTimestamp("expires_at").toInstant();
193+
return new DefaultOneTimeToken(tokenValue, userName, expiresAt);
194+
}
195+
196+
}
197+
198+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2002-2024 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.authentication.ott;
18+
19+
import java.time.Clock;
20+
import java.time.Instant;
21+
import java.util.UUID;
22+
23+
import org.springframework.util.Assert;
24+
25+
/**
26+
*
27+
* Utility for default generation and checking of {@link OneTimeToken} time to live.
28+
*
29+
* @author Max Batischev
30+
* @since 6.4
31+
*/
32+
public final class OneTimeTokenUtils {
33+
34+
public static long DEFAULT_ONE_TIME_TOKEN_TIME_TO_LIVE = 300;
35+
36+
private static Clock clock = Clock.systemUTC();
37+
38+
private OneTimeTokenUtils() {
39+
}
40+
41+
public static OneTimeToken generateOneTimeToken(GenerateOneTimeTokenRequest request, long timeToLive) {
42+
Assert.notNull(request, "generateOneTimeTokenRequest cannot be null");
43+
Assert.isTrue(!(timeToLive <= 0), "timeToLive must be greater than 0");
44+
45+
String token = UUID.randomUUID().toString();
46+
Instant fiveMinutesFromNow = clock.instant().plusSeconds(timeToLive);
47+
return new DefaultOneTimeToken(token, request.getUsername(), fiveMinutesFromNow);
48+
}
49+
50+
public static boolean isExpired(OneTimeToken token) {
51+
Assert.notNull(token, "oneTimeToken cannot be null");
52+
return clock.instant().isAfter(token.getExpiresAt());
53+
}
54+
55+
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
org.springframework.aot.hint.RuntimeHintsRegistrar=\
2-
org.springframework.security.aot.hint.CoreSecurityRuntimeHints
2+
org.springframework.security.aot.hint.CoreSecurityRuntimeHints,\
3+
org.springframework.security.aot.hint.OneTimeTokenRuntimeHints
4+
35
org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\
46
org.springframework.security.aot.hint.SecurityHintsAotProcessor
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
create table one_time_tokens
2+
(
3+
token_value varchar(36) not null primary key,
4+
username varchar_ignorecase(50) not null,
5+
expires_at timestamp not null
6+
);

0 commit comments

Comments
 (0)