From a59913bb381b55772bb299a7f1babdc3e75aec17 Mon Sep 17 00:00:00 2001 From: Zakaria Date: Sat, 24 Oct 2026 14:50:02 -0400 Subject: [PATCH] fix(cognito): keep revoked tokens across restarts RevokedTokenInfo serialized its derived isExpired() as "expired", which the persistent store's ObjectMapper then refused to read back. On restart the file was moved aside as .corrupt and every revocation was forgotten, so refresh tokens revoked by sign-out worked again. The derived field is no longer written, and files that carry it are read with it ignored. --- .../cognito/model/RevokedTokenInfo.java | 7 +++ .../RevokedTokenInfoPersistenceTest.java | 44 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 src/test/java/io/github/hectorvent/floci/services/cognito/RevokedTokenInfoPersistenceTest.java diff --git a/src/main/java/io/github/hectorvent/floci/services/cognito/model/RevokedTokenInfo.java b/src/main/java/io/github/hectorvent/floci/services/cognito/model/RevokedTokenInfo.java index ac77da8..2942aa4 100644 --- a/src/main/java/io/github/hectorvent/floci/services/cognito/model/RevokedTokenInfo.java +++ b/src/main/java/io/github/hectorvent/floci/services/cognito/model/RevokedTokenInfo.java @@ -3,13 +3,19 @@ package io.github.hectorvent.floci.services.cognito.model; import io.quarkus.runtime.annotations.RegisterForReflection; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; /** * Information about a revoked token for tracking and validation purposes. * Used to implement AWS Cognito AdminUserGlobalSignOut token revocation behavior. + * + *

{@code expired} is derived from {@code expiresAt}, so it is not stored. Files written before + * that still carry it, and are read with it ignored instead of being discarded as corrupt. */ @RegisterForReflection +@JsonIgnoreProperties(ignoreUnknown = true) public class RevokedTokenInfo { private final String jti; @@ -62,6 +68,7 @@ public class RevokedTokenInfo { /** * Check if this revocation record has expired and can be cleaned up. */ + @JsonIgnore public boolean isExpired() { return System.currentTimeMillis() / 1000L > expiresAt; } diff --git a/src/test/java/io/github/hectorvent/floci/services/cognito/RevokedTokenInfoPersistenceTest.java b/src/test/java/io/github/hectorvent/floci/services/cognito/RevokedTokenInfoPersistenceTest.java new file mode 100644 index 0000000..0a3c263 --- /dev/null +++ b/src/test/java/io/github/hectorvent/floci/services/cognito/RevokedTokenInfoPersistenceTest.java @@ -0,0 +1,44 @@ +package io.github.hectorvent.floci.services.cognito; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.github.hectorvent.floci.services.cognito.model.RevokedTokenInfo; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +/** Revoked tokens must survive a restart, which reads them back with a strict ObjectMapper. */ +class RevokedTokenInfoPersistenceTest { + + private static final TypeReference> TYPE = new TypeReference<>() { + }; + private final ObjectMapper strictMapper = new ObjectMapper(); + + @Test + void roundTripsWithoutTheDerivedExpiredField() throws Exception { + RevokedTokenInfo info = new RevokedTokenInfo("jti-1", "refresh", "alice", "us-east-1_pool", 1_000L, 4_102_444_800L); + + String json = strictMapper.writeValueAsString(Map.of("k", info)); + RevokedTokenInfo read = strictMapper.readValue(json, TYPE).get("k"); + + assertFalse(json.contains("\"expired\""), json); + assertEquals("jti-1", read.getJti()); + assertEquals(4_102_444_800L, read.getExpiresAt()); + } + + @Test + void readsFilesWrittenWithTheExpiredField() throws Exception { + String saved = """ + {"000000000000/revoked:us-east-1_pool:jti-1":{"jti":"jti-1","tokenType":"refresh","username":"alice", + "userPoolId":"us-east-1_pool","revokedAt":1792858112669,"expiresAt":1824394112,"expired":false}} + """; + + RevokedTokenInfo read = strictMapper.readValue(saved, TYPE).get("000000000000/revoked:us-east-1_pool:jti-1"); + + assertEquals("alice", read.getUsername()); + assertEquals(1824394112L, read.getExpiresAt()); + } +} -- 2.52.0