test(security): add comprehensive tests for noncemanager ttl, concurrency and clock skew
This commit is contained in:
+30
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
+80
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user