Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds OneTimeTokenSettings #16260

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,26 @@
*/
public final class InMemoryOneTimeTokenService implements OneTimeTokenService {

private final OneTimeTokenSettings oneTimeTokenSettings;

public InMemoryOneTimeTokenService() {
this(null);
}

/**
* Constructs a {@code InMemoryOneTimeTokenService} using the provided parameters.
* @param oneTimeTokenSettings the {@link OneTimeTokenSettings} to use when generating
* OneTimeTokens
* @since 6.4.2
* @see OneTimeTokenSettings
*/
public InMemoryOneTimeTokenService(OneTimeTokenSettings oneTimeTokenSettings) {
if (oneTimeTokenSettings == null) {
oneTimeTokenSettings = OneTimeTokenSettings.withDefaults().build();
}
this.oneTimeTokenSettings = oneTimeTokenSettings;
}

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

private Clock clock = Clock.systemUTC();
Expand All @@ -44,8 +64,8 @@ public final class InMemoryOneTimeTokenService implements OneTimeTokenService {
@NonNull
public OneTimeToken generate(GenerateOneTimeTokenRequest request) {
String token = UUID.randomUUID().toString();
Instant fiveMinutesFromNow = this.clock.instant().plusSeconds(300);
OneTimeToken ott = new DefaultOneTimeToken(token, request.getUsername(), fiveMinutesFromNow);
Instant expireTime = this.clock.instant().plus(this.oneTimeTokenSettings.getTimeToLive());
OneTimeToken ott = new DefaultOneTimeToken(token, request.getUsername(), expireTime);
this.oneTimeTokenByToken.put(token, ott);
cleanExpiredTokensIfNeeded();
return ott;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import java.sql.Timestamp;
import java.sql.Types;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
Expand Down Expand Up @@ -63,6 +62,8 @@ public final class JdbcOneTimeTokenService implements OneTimeTokenService, Dispo

private final JdbcOperations jdbcOperations;

private final OneTimeTokenSettings oneTimeTokenSettings;

private Function<OneTimeToken, List<SqlParameterValue>> oneTimeTokenParametersMapper = new OneTimeTokenParametersMapper();

private RowMapper<OneTimeToken> oneTimeTokenRowMapper = new OneTimeTokenRowMapper();
Expand Down Expand Up @@ -107,9 +108,25 @@ public final class JdbcOneTimeTokenService implements OneTimeTokenService, Dispo
* @param jdbcOperations the JDBC operations
*/
public JdbcOneTimeTokenService(JdbcOperations jdbcOperations) {
this(jdbcOperations, null);
}

/**
* Constructs a {@code JdbcOneTimeTokenService} using the provided parameters.
* @param jdbcOperations the JDBC operations
* @param oneTimeTokenSettings the {@link OneTimeTokenSettings} to use when generating
* OneTimeTokens
* @since 6.4.2
* @see OneTimeTokenSettings
*/
public JdbcOneTimeTokenService(JdbcOperations jdbcOperations, OneTimeTokenSettings oneTimeTokenSettings) {
Assert.notNull(jdbcOperations, "jdbcOperations cannot be null");
this.jdbcOperations = jdbcOperations;
this.taskScheduler = createTaskScheduler(DEFAULT_CLEANUP_CRON);
if (oneTimeTokenSettings == null) {
oneTimeTokenSettings = OneTimeTokenSettings.withDefaults().build();
}
this.oneTimeTokenSettings = oneTimeTokenSettings;
}

/**
Expand All @@ -132,8 +149,8 @@ public void setCleanupCron(String cleanupCron) {
public OneTimeToken generate(GenerateOneTimeTokenRequest request) {
Assert.notNull(request, "generateOneTimeTokenRequest cannot be null");
String token = UUID.randomUUID().toString();
Instant fiveMinutesFromNow = this.clock.instant().plus(Duration.ofMinutes(5));
OneTimeToken oneTimeToken = new DefaultOneTimeToken(token, request.getUsername(), fiveMinutesFromNow);
Instant expireTime = this.clock.instant().plus(this.oneTimeTokenSettings.getTimeToLive());
OneTimeToken oneTimeToken = new DefaultOneTimeToken(token, request.getUsername(), expireTime);
insertOneTimeToken(oneTimeToken);
return oneTimeToken;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.security.authentication.ott;

import java.time.Duration;

/**
* A facility for {@link OneTimeToken} configuration settings.
*
* @author Micah Moore (Zetetic LLC)
* @since 6.4.2
*/
public final class OneTimeTokenSettings {

private static final Duration DEFAULT_TIME_TO_LIVE = Duration.ofMinutes(5L);

private final Duration timeToLive;

private OneTimeTokenSettings(Duration timeToLive) {
this.timeToLive = timeToLive;
}

public Duration getTimeToLive() {
return this.timeToLive;
}

public static Builder withDefaults() {
return new Builder(DEFAULT_TIME_TO_LIVE);
}

public static final class Builder {

private Duration timeToLive;

Builder(Duration timeToLive) {
this.timeToLive = timeToLive;
}

public Builder timeToLive(Duration timeToLive) {
this.timeToLive = timeToLive;
return this;
}

public OneTimeTokenSettings build() {
return new OneTimeTokenSettings(this.timeToLive);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.springframework.security.authentication.ott;

import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
Expand All @@ -38,8 +39,13 @@
*/
class InMemoryOneTimeTokenServiceTests {

static final Duration CUSTOM_EXPIRE_DURATION = Duration.ofMinutes(30L);

InMemoryOneTimeTokenService oneTimeTokenService = new InMemoryOneTimeTokenService();

InMemoryOneTimeTokenService oneTimeTokenServiceWithTokenSettings = new InMemoryOneTimeTokenService(
OneTimeTokenSettings.withDefaults().timeToLive(CUSTOM_EXPIRE_DURATION).build());

@Test
void generateThenTokenValueShouldBeValidUuidAndProvidedUsernameIsUsed() {
GenerateOneTimeTokenRequest request = new GenerateOneTimeTokenRequest("user");
Expand Down Expand Up @@ -110,6 +116,41 @@ void setClockWhenNullThenThrowIllegalArgumentException() {
// @formatter:on
}

@Test
void generateOneTimeTokenWithCustomExpireTimeThenTokenShouldHaveCustomExpiration() {
GenerateOneTimeTokenRequest request = new GenerateOneTimeTokenRequest("user");
OneTimeToken generated = this.oneTimeTokenServiceWithTokenSettings.generate(request);
Instant twentyNineMinutesFromNow = Instant.now().plus(29, ChronoUnit.MINUTES);
Instant thirtyOneMinutesFromNow = Instant.now().plus(31, ChronoUnit.MINUTES);
assertThat(generated.getExpiresAt()).isBetween(twentyNineMinutesFromNow, thirtyOneMinutesFromNow);
}

@Test
void consumeWhenTokenWithCustomExpireTimeIsValidThenReturnToken() {
GenerateOneTimeTokenRequest request = new GenerateOneTimeTokenRequest("user");
OneTimeToken generated = this.oneTimeTokenServiceWithTokenSettings.generate(request);
OneTimeTokenAuthenticationToken authenticationToken = new OneTimeTokenAuthenticationToken(
generated.getTokenValue());
Clock fifteenMinutesFromNow = Clock.fixed(Instant.now().plus(15, ChronoUnit.MINUTES), ZoneOffset.UTC);
this.oneTimeTokenServiceWithTokenSettings.setClock(fifteenMinutesFromNow);
OneTimeToken consumed = this.oneTimeTokenServiceWithTokenSettings.consume(authenticationToken);
assertThat(consumed.getTokenValue()).isEqualTo(generated.getTokenValue());
assertThat(consumed.getUsername()).isEqualTo(generated.getUsername());
assertThat(consumed.getExpiresAt()).isEqualTo(generated.getExpiresAt());
}

@Test
void consumeWhenTokenWithCustomExpireTimeIsExpiredThenReturnNull() {
GenerateOneTimeTokenRequest request = new GenerateOneTimeTokenRequest("user");
OneTimeToken generated = this.oneTimeTokenServiceWithTokenSettings.generate(request);
OneTimeTokenAuthenticationToken authenticationToken = new OneTimeTokenAuthenticationToken(
generated.getTokenValue());
Clock thirtyOneMinutesFromNow = Clock.fixed(Instant.now().plus(31, ChronoUnit.MINUTES), ZoneOffset.UTC);
this.oneTimeTokenServiceWithTokenSettings.setClock(thirtyOneMinutesFromNow);
OneTimeToken consumed = this.oneTimeTokenService.consume(authenticationToken);
assertThat(consumed).isNull();
}

private List<OneTimeToken> generate(int howMany) {
List<OneTimeToken> generated = new ArrayList<>(howMany);
for (int i = 0; i < howMany; i++) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,23 +50,30 @@ class JdbcOneTimeTokenServiceTests {

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

static final Duration CUSTOM_EXPIRE_DURATION = Duration.ofMinutes(30L);

private EmbeddedDatabase db;

private JdbcOperations jdbcOperations;

private JdbcOneTimeTokenService oneTimeTokenService;

private JdbcOneTimeTokenService oneTimeTokenServiceWithTokenSettings;

@BeforeEach
void setUp() {
this.db = createDb();
this.jdbcOperations = new JdbcTemplate(this.db);
this.oneTimeTokenService = new JdbcOneTimeTokenService(this.jdbcOperations);
this.oneTimeTokenServiceWithTokenSettings = new JdbcOneTimeTokenService(this.jdbcOperations,
OneTimeTokenSettings.withDefaults().timeToLive(CUSTOM_EXPIRE_DURATION).build());
}

@AfterEach
void tearDown() throws Exception {
this.db.shutdown();
this.oneTimeTokenService.destroy();
this.oneTimeTokenServiceWithTokenSettings.destroy();
}

private static EmbeddedDatabase createDb() {
Expand Down Expand Up @@ -188,4 +195,39 @@ void setCleanupChronWhenAlreadyNullThenNoException() {
this.oneTimeTokenService.setCleanupCron(null);
}

@Test
void generateOneTimeTokenWithCustomExpireTimeThenTokenShouldHaveCustomExpiration() {
GenerateOneTimeTokenRequest request = new GenerateOneTimeTokenRequest(USERNAME);
OneTimeToken generated = this.oneTimeTokenServiceWithTokenSettings.generate(request);
Instant twentyNineMinutesFromNow = Instant.now().plus(29, ChronoUnit.MINUTES);
Instant thirtyOneMinutesFromNow = Instant.now().plus(31, ChronoUnit.MINUTES);
assertThat(generated.getExpiresAt()).isBetween(twentyNineMinutesFromNow, thirtyOneMinutesFromNow);
}

@Test
void consumeWhenTokenWithCustomExpireTimeIsValidThenReturnToken() {
GenerateOneTimeTokenRequest request = new GenerateOneTimeTokenRequest(USERNAME);
OneTimeToken generated = this.oneTimeTokenServiceWithTokenSettings.generate(request);
OneTimeTokenAuthenticationToken authenticationToken = new OneTimeTokenAuthenticationToken(
generated.getTokenValue());
Clock fifteenMinutesFromNow = Clock.fixed(Instant.now().plus(15, ChronoUnit.MINUTES), ZoneOffset.UTC);
this.oneTimeTokenServiceWithTokenSettings.setClock(fifteenMinutesFromNow);
OneTimeToken consumed = this.oneTimeTokenServiceWithTokenSettings.consume(authenticationToken);
assertThat(consumed.getTokenValue()).isEqualTo(generated.getTokenValue());
assertThat(consumed.getUsername()).isEqualTo(generated.getUsername());
assertThat(consumed.getExpiresAt()).isEqualTo(generated.getExpiresAt());
}

@Test
void consumeWhenTokenWithCustomExpireTimeIsExpiredThenReturnNull() {
GenerateOneTimeTokenRequest request = new GenerateOneTimeTokenRequest(USERNAME);
OneTimeToken generated = this.oneTimeTokenServiceWithTokenSettings.generate(request);
OneTimeTokenAuthenticationToken authenticationToken = new OneTimeTokenAuthenticationToken(
generated.getTokenValue());
Clock thirtyOneMinutesFromNow = Clock.fixed(Instant.now().plus(31, ChronoUnit.MINUTES), ZoneOffset.UTC);
this.oneTimeTokenServiceWithTokenSettings.setClock(thirtyOneMinutesFromNow);
OneTimeToken consumed = this.oneTimeTokenServiceWithTokenSettings.consume(authenticationToken);
assertThat(consumed).isNull();
}

}