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.
This commit is contained in:
@@ -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.
|
||||
*
|
||||
* <p>{@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;
|
||||
}
|
||||
|
||||
+44
@@ -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<Map<String, RevokedTokenInfo>> 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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user