test(security): add comprehensive tests for noncemanager ttl, concurrency and clock skew

This commit is contained in:
Claudecio Martins
2026-10-20 15:00:50 -03:00
parent a562909bc5
commit 7aa33a4789
2 changed files with 110 additions and 0 deletions
@@ -10,6 +10,8 @@ import java.util.concurrent.ConcurrentHashMap;
*/ */
public class NonceManager { public class NonceManager {
public static final Duration DEFAULT_CLOCK_SKEW = Duration.ofSeconds(30);
private final Duration windowDuration; private final Duration windowDuration;
private final ConcurrentHashMap<String, Instant> usedNonces = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, Instant> usedNonces = new ConcurrentHashMap<>();
@@ -21,6 +23,34 @@ public class NonceManager {
this(Duration.ofSeconds(120)); this(Duration.ofSeconds(120));
} }
/**
* Valida um timestamp de requisição em relação ao relógio local, considerando a janela
* de validade e uma margem segura de clock skew (tolerância a desvio de relógio entre cliente e agente).
*
* @param requestTimestamp Instant da requisição informado pelo cliente.
* @param clockSkew Tolerância de tolerância a skew de relógio.
* @return {@code true} se o timestamp estiver na faixa válida permitida.
*/
public boolean isTimestampValid(Instant requestTimestamp, Duration clockSkew) {
if (requestTimestamp == null) {
return false;
}
Duration skew = clockSkew != null ? clockSkew : DEFAULT_CLOCK_SKEW;
Instant now = Instant.now();
Instant earliestAllowed = now.minus(windowDuration).minus(skew);
Instant latestAllowed = now.plus(skew);
return !requestTimestamp.isBefore(earliestAllowed) && !requestTimestamp.isAfter(latestAllowed);
}
public boolean isTimestampValid(Instant requestTimestamp) {
return isTimestampValid(requestTimestamp, DEFAULT_CLOCK_SKEW);
}
public Duration getWindowDuration() {
return windowDuration;
}
/** /**
* Tenta registrar um nonce. * Tenta registrar um nonce.
* *
@@ -28,4 +28,84 @@ class NonceManagerTest {
assertThat(manager.registerNonce(null)).isFalse(); assertThat(manager.registerNonce(null)).isFalse();
assertThat(manager.registerNonce(" ")).isFalse(); assertThat(manager.registerNonce(" ")).isFalse();
} }
@Test
@DisplayName("Deve permitir apenas 1 registro concorrente simultâneo para o mesmo nonce")
void shouldBeThreadSafeUnderHighConcurrency() throws Exception {
NonceManager manager = new NonceManager();
String identicalNonce = "shared-race-nonce-" + java.util.UUID.randomUUID();
int threads = 30;
java.util.concurrent.ExecutorService executor = java.util.concurrent.Executors.newFixedThreadPool(threads);
java.util.concurrent.CountDownLatch startLatch = new java.util.concurrent.CountDownLatch(1);
java.util.concurrent.atomic.AtomicInteger successes = new java.util.concurrent.atomic.AtomicInteger(0);
java.util.concurrent.atomic.AtomicInteger rejections = new java.util.concurrent.atomic.AtomicInteger(0);
java.util.List<java.util.concurrent.Future<?>> futures = new java.util.ArrayList<>();
for (int i = 0; i < threads; i++) {
futures.add(executor.submit(() -> {
try {
startLatch.await();
boolean registered = manager.registerNonce(identicalNonce);
if (registered) {
successes.incrementAndGet();
} else {
rejections.incrementAndGet();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}));
}
startLatch.countDown();
for (java.util.concurrent.Future<?> f : futures) {
f.get();
}
executor.shutdown();
assertThat(successes.get()).isEqualTo(1);
assertThat(rejections.get()).isEqualTo(threads - 1);
assertThat(manager.activeNonceCount()).isEqualTo(1);
}
@Test
@DisplayName("Deve validar timestamp com tolerância a clock skew (passado recente e pequeno futuro)")
void shouldValidateTimestampWithClockSkew() {
NonceManager manager = new NonceManager(Duration.ofSeconds(60));
java.time.Instant now = java.time.Instant.now();
// Timestamp atual é válido
assertThat(manager.isTimestampValid(now)).isTrue();
// 10 segundos no passado (dentro da janela de 60s) é válido
assertThat(manager.isTimestampValid(now.minusSeconds(10))).isTrue();
// 20 segundos no futuro (dentro do clock skew de 30s) é tolerado
assertThat(manager.isTimestampValid(now.plusSeconds(20))).isTrue();
// 100 segundos no passado (fora de 60s + 30s = 90s) é rejeitado
assertThat(manager.isTimestampValid(now.minusSeconds(100))).isFalse();
// 60 segundos no futuro (além do skew de 30s) é rejeitado
assertThat(manager.isTimestampValid(now.plusSeconds(60))).isFalse();
// Nulo é rejeitado
assertThat(manager.isTimestampValid(null)).isFalse();
}
@Test
@DisplayName("Deve limpar nonces expirados além da janela configurada")
void shouldCleanupExpiredNonces() throws Exception {
NonceManager manager = new NonceManager(Duration.ofMillis(50));
manager.registerNonce("temporary-nonce");
assertThat(manager.activeNonceCount()).isEqualTo(1);
Thread.sleep(70);
manager.cleanupExpired();
assertThat(manager.activeNonceCount()).isEqualTo(0);
// Após expirar e ser limpo, pode ser aceito novamente
assertThat(manager.registerNonce("temporary-nonce")).isTrue();
}
} }