Skip to content

Commit 8d64036

Browse files
committed
Adds OneTimeTokenSettings
Provided to OneTimeTokenService constructors to customize expire time when generating OneTimeToken
1 parent 9901530 commit 8d64036

File tree

5 files changed

+186
-5
lines changed

5 files changed

+186
-5
lines changed

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

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,26 @@
3636
*/
3737
public final class InMemoryOneTimeTokenService implements OneTimeTokenService {
3838

39+
private final OneTimeTokenSettings oneTimeTokenSettings;
40+
41+
public InMemoryOneTimeTokenService() {
42+
this(null);
43+
}
44+
45+
/**
46+
* Constructs a {@code InMemoryOneTimeTokenService} using the provided parameters.
47+
* @param oneTimeTokenSettings the {@link OneTimeTokenSettings} to use when generating
48+
* OneTimeTokens
49+
* @since 6.4.2
50+
* @see OneTimeTokenSettings
51+
*/
52+
public InMemoryOneTimeTokenService(OneTimeTokenSettings oneTimeTokenSettings) {
53+
if (oneTimeTokenSettings == null) {
54+
oneTimeTokenSettings = OneTimeTokenSettings.withDefaults().build();
55+
}
56+
this.oneTimeTokenSettings = oneTimeTokenSettings;
57+
}
58+
3959
private final Map<String, OneTimeToken> oneTimeTokenByToken = new ConcurrentHashMap<>();
4060

4161
private Clock clock = Clock.systemUTC();
@@ -44,8 +64,8 @@ public final class InMemoryOneTimeTokenService implements OneTimeTokenService {
4464
@NonNull
4565
public OneTimeToken generate(GenerateOneTimeTokenRequest request) {
4666
String token = UUID.randomUUID().toString();
47-
Instant fiveMinutesFromNow = this.clock.instant().plusSeconds(300);
48-
OneTimeToken ott = new DefaultOneTimeToken(token, request.getUsername(), fiveMinutesFromNow);
67+
Instant expireTime = this.clock.instant().plus(this.oneTimeTokenSettings.getTimeToLive());
68+
OneTimeToken ott = new DefaultOneTimeToken(token, request.getUsername(), expireTime);
4969
this.oneTimeTokenByToken.put(token, ott);
5070
cleanExpiredTokensIfNeeded();
5171
return ott;

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

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import java.sql.Timestamp;
2222
import java.sql.Types;
2323
import java.time.Clock;
24-
import java.time.Duration;
2524
import java.time.Instant;
2625
import java.util.ArrayList;
2726
import java.util.List;
@@ -63,6 +62,8 @@ public final class JdbcOneTimeTokenService implements OneTimeTokenService, Dispo
6362

6463
private final JdbcOperations jdbcOperations;
6564

65+
private final OneTimeTokenSettings oneTimeTokenSettings;
66+
6667
private Function<OneTimeToken, List<SqlParameterValue>> oneTimeTokenParametersMapper = new OneTimeTokenParametersMapper();
6768

6869
private RowMapper<OneTimeToken> oneTimeTokenRowMapper = new OneTimeTokenRowMapper();
@@ -107,9 +108,25 @@ public final class JdbcOneTimeTokenService implements OneTimeTokenService, Dispo
107108
* @param jdbcOperations the JDBC operations
108109
*/
109110
public JdbcOneTimeTokenService(JdbcOperations jdbcOperations) {
111+
this(jdbcOperations, null);
112+
}
113+
114+
/**
115+
* Constructs a {@code JdbcOneTimeTokenService} using the provided parameters.
116+
* @param jdbcOperations the JDBC operations
117+
* @param oneTimeTokenSettings the {@link OneTimeTokenSettings} to use when generating
118+
* OneTimeTokens
119+
* @since 6.4.2
120+
* @see OneTimeTokenSettings
121+
*/
122+
public JdbcOneTimeTokenService(JdbcOperations jdbcOperations, OneTimeTokenSettings oneTimeTokenSettings) {
110123
Assert.notNull(jdbcOperations, "jdbcOperations cannot be null");
111124
this.jdbcOperations = jdbcOperations;
112125
this.taskScheduler = createTaskScheduler(DEFAULT_CLEANUP_CRON);
126+
if (oneTimeTokenSettings == null) {
127+
oneTimeTokenSettings = OneTimeTokenSettings.withDefaults().build();
128+
}
129+
this.oneTimeTokenSettings = oneTimeTokenSettings;
113130
}
114131

115132
/**
@@ -132,8 +149,8 @@ public void setCleanupCron(String cleanupCron) {
132149
public OneTimeToken generate(GenerateOneTimeTokenRequest request) {
133150
Assert.notNull(request, "generateOneTimeTokenRequest cannot be null");
134151
String token = UUID.randomUUID().toString();
135-
Instant fiveMinutesFromNow = this.clock.instant().plus(Duration.ofMinutes(5));
136-
OneTimeToken oneTimeToken = new DefaultOneTimeToken(token, request.getUsername(), fiveMinutesFromNow);
152+
Instant expireTime = this.clock.instant().plus(this.oneTimeTokenSettings.getTimeToLive());
153+
OneTimeToken oneTimeToken = new DefaultOneTimeToken(token, request.getUsername(), expireTime);
137154
insertOneTimeToken(oneTimeToken);
138155
return oneTimeToken;
139156
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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.Duration;
20+
21+
/**
22+
* A facility for {@link OneTimeToken} configuration settings.
23+
*
24+
* @author Micah Moore (Zetetic LLC)
25+
* @since 6.4.2
26+
*/
27+
public final class OneTimeTokenSettings {
28+
29+
private static final Duration DEFAULT_TIME_TO_LIVE = Duration.ofMinutes(5L);
30+
31+
private final Duration timeToLive;
32+
33+
private OneTimeTokenSettings(Duration timeToLive) {
34+
this.timeToLive = timeToLive;
35+
}
36+
37+
public Duration getTimeToLive() {
38+
return this.timeToLive;
39+
}
40+
41+
public static Builder withDefaults() {
42+
return new Builder(DEFAULT_TIME_TO_LIVE);
43+
}
44+
45+
public static final class Builder {
46+
47+
private Duration timeToLive;
48+
49+
Builder(Duration timeToLive) {
50+
this.timeToLive = timeToLive;
51+
}
52+
53+
public Builder timeToLive(Duration timeToLive) {
54+
this.timeToLive = timeToLive;
55+
return this;
56+
}
57+
58+
public OneTimeTokenSettings build() {
59+
return new OneTimeTokenSettings(this.timeToLive);
60+
}
61+
62+
}
63+
64+
}

core/src/test/java/org/springframework/security/authentication/ott/InMemoryOneTimeTokenServiceTests.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.security.authentication.ott;
1818

1919
import java.time.Clock;
20+
import java.time.Duration;
2021
import java.time.Instant;
2122
import java.time.ZoneOffset;
2223
import java.time.temporal.ChronoUnit;
@@ -37,9 +38,12 @@
3738
* @author Marcus da Coregio
3839
*/
3940
class InMemoryOneTimeTokenServiceTests {
41+
static final Duration CUSTOM_EXPIRE_DURATION = Duration.ofMinutes(30L);
4042

4143
InMemoryOneTimeTokenService oneTimeTokenService = new InMemoryOneTimeTokenService();
4244

45+
InMemoryOneTimeTokenService oneTimeTokenServiceWithTokenSettings = new InMemoryOneTimeTokenService(
46+
OneTimeTokenSettings.withDefaults().timeToLive(CUSTOM_EXPIRE_DURATION).build());
4347
@Test
4448
void generateThenTokenValueShouldBeValidUuidAndProvidedUsernameIsUsed() {
4549
GenerateOneTimeTokenRequest request = new GenerateOneTimeTokenRequest("user");
@@ -110,6 +114,41 @@ void setClockWhenNullThenThrowIllegalArgumentException() {
110114
// @formatter:on
111115
}
112116

117+
@Test
118+
void generateOneTimeTokenWithCustomExpireTimeThenTokenShouldHaveCustomExpiration() {
119+
GenerateOneTimeTokenRequest request = new GenerateOneTimeTokenRequest("user");
120+
OneTimeToken generated = this.oneTimeTokenServiceWithTokenSettings.generate(request);
121+
Instant twentyNineMinutesFromNow = Instant.now().plus(29, ChronoUnit.MINUTES);
122+
Instant thirtyOneMinutesFromNow = Instant.now().plus(31, ChronoUnit.MINUTES);
123+
assertThat(generated.getExpiresAt()).isBetween(twentyNineMinutesFromNow, thirtyOneMinutesFromNow);
124+
}
125+
126+
@Test
127+
void consumeWhenTokenWithCustomExpireTimeIsValidThenReturnToken() {
128+
GenerateOneTimeTokenRequest request = new GenerateOneTimeTokenRequest("user");
129+
OneTimeToken generated = this.oneTimeTokenServiceWithTokenSettings.generate(request);
130+
OneTimeTokenAuthenticationToken authenticationToken = new OneTimeTokenAuthenticationToken(
131+
generated.getTokenValue());
132+
Clock fifteenMinutesFromNow = Clock.fixed(Instant.now().plus(15, ChronoUnit.MINUTES), ZoneOffset.UTC);
133+
this.oneTimeTokenServiceWithTokenSettings.setClock(fifteenMinutesFromNow);
134+
OneTimeToken consumed = this.oneTimeTokenServiceWithTokenSettings.consume(authenticationToken);
135+
assertThat(consumed.getTokenValue()).isEqualTo(generated.getTokenValue());
136+
assertThat(consumed.getUsername()).isEqualTo(generated.getUsername());
137+
assertThat(consumed.getExpiresAt()).isEqualTo(generated.getExpiresAt());
138+
}
139+
140+
@Test
141+
void consumeWhenTokenWithCustomExpireTimeIsExpiredThenReturnNull() {
142+
GenerateOneTimeTokenRequest request = new GenerateOneTimeTokenRequest("user");
143+
OneTimeToken generated = this.oneTimeTokenServiceWithTokenSettings.generate(request);
144+
OneTimeTokenAuthenticationToken authenticationToken = new OneTimeTokenAuthenticationToken(
145+
generated.getTokenValue());
146+
Clock thirtyOneMinutesFromNow = Clock.fixed(Instant.now().plus(31, ChronoUnit.MINUTES), ZoneOffset.UTC);
147+
this.oneTimeTokenServiceWithTokenSettings.setClock(thirtyOneMinutesFromNow);
148+
OneTimeToken consumed = this.oneTimeTokenService.consume(authenticationToken);
149+
assertThat(consumed).isNull();
150+
}
151+
113152
private List<OneTimeToken> generate(int howMany) {
114153
List<OneTimeToken> generated = new ArrayList<>(howMany);
115154
for (int i = 0; i < howMany; i++) {

core/src/test/java/org/springframework/security/authentication/ott/JdbcOneTimeTokenServiceTests.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,23 +50,30 @@ class JdbcOneTimeTokenServiceTests {
5050

5151
private static final String ONE_TIME_TOKEN_SQL_RESOURCE = "org/springframework/security/core/ott/jdbc/one-time-tokens-schema.sql";
5252

53+
static final Duration CUSTOM_EXPIRE_DURATION = Duration.ofMinutes(30L);
54+
5355
private EmbeddedDatabase db;
5456

5557
private JdbcOperations jdbcOperations;
5658

5759
private JdbcOneTimeTokenService oneTimeTokenService;
5860

61+
private JdbcOneTimeTokenService oneTimeTokenServiceWithTokenSettings;
62+
5963
@BeforeEach
6064
void setUp() {
6165
this.db = createDb();
6266
this.jdbcOperations = new JdbcTemplate(this.db);
6367
this.oneTimeTokenService = new JdbcOneTimeTokenService(this.jdbcOperations);
68+
this.oneTimeTokenServiceWithTokenSettings = new JdbcOneTimeTokenService(this.jdbcOperations,
69+
OneTimeTokenSettings.withDefaults().timeToLive(CUSTOM_EXPIRE_DURATION).build());
6470
}
6571

6672
@AfterEach
6773
void tearDown() throws Exception {
6874
this.db.shutdown();
6975
this.oneTimeTokenService.destroy();
76+
this.oneTimeTokenServiceWithTokenSettings.destroy();
7077
}
7178

7279
private static EmbeddedDatabase createDb() {
@@ -188,4 +195,38 @@ void setCleanupChronWhenAlreadyNullThenNoException() {
188195
this.oneTimeTokenService.setCleanupCron(null);
189196
}
190197

198+
@Test
199+
void generateOneTimeTokenWithCustomExpireTimeThenTokenShouldHaveCustomExpiration() {
200+
GenerateOneTimeTokenRequest request = new GenerateOneTimeTokenRequest(USERNAME);
201+
OneTimeToken generated = this.oneTimeTokenServiceWithTokenSettings.generate(request);
202+
Instant twentyNineMinutesFromNow = Instant.now().plus(29, ChronoUnit.MINUTES);
203+
Instant thirtyOneMinutesFromNow = Instant.now().plus(31, ChronoUnit.MINUTES);
204+
assertThat(generated.getExpiresAt()).isBetween(twentyNineMinutesFromNow, thirtyOneMinutesFromNow);
205+
}
206+
207+
@Test
208+
void consumeWhenTokenWithCustomExpireTimeIsValidThenReturnToken() {
209+
GenerateOneTimeTokenRequest request = new GenerateOneTimeTokenRequest(USERNAME);
210+
OneTimeToken generated = this.oneTimeTokenServiceWithTokenSettings.generate(request);
211+
OneTimeTokenAuthenticationToken authenticationToken = new OneTimeTokenAuthenticationToken(
212+
generated.getTokenValue());
213+
Clock fifteenMinutesFromNow = Clock.fixed(Instant.now().plus(15, ChronoUnit.MINUTES), ZoneOffset.UTC);
214+
this.oneTimeTokenServiceWithTokenSettings.setClock(fifteenMinutesFromNow);
215+
OneTimeToken consumed = this.oneTimeTokenServiceWithTokenSettings.consume(authenticationToken);
216+
assertThat(consumed.getTokenValue()).isEqualTo(generated.getTokenValue());
217+
assertThat(consumed.getUsername()).isEqualTo(generated.getUsername());
218+
assertThat(consumed.getExpiresAt()).isEqualTo(generated.getExpiresAt());
219+
}
220+
221+
@Test
222+
void consumeWhenTokenWithCustomExpireTimeIsExpiredThenReturnNull() {
223+
GenerateOneTimeTokenRequest request = new GenerateOneTimeTokenRequest(USERNAME);
224+
OneTimeToken generated = this.oneTimeTokenServiceWithTokenSettings.generate(request);
225+
OneTimeTokenAuthenticationToken authenticationToken = new OneTimeTokenAuthenticationToken(
226+
generated.getTokenValue());
227+
Clock thirtyOneMinutesFromNow = Clock.fixed(Instant.now().plus(31, ChronoUnit.MINUTES), ZoneOffset.UTC);
228+
this.oneTimeTokenServiceWithTokenSettings.setClock(thirtyOneMinutesFromNow);
229+
OneTimeToken consumed = this.oneTimeTokenServiceWithTokenSettings.consume(authenticationToken);
230+
assertThat(consumed).isNull();
231+
}
191232
}

0 commit comments

Comments
 (0)