Last active
October 9, 2025 14:04
-
-
Save thomasdarimont/709de42f09598d210fbfa9cdad9f4d3f to your computer and use it in GitHub Desktop.
Initial tests for working with Token Status List (TSL) compressed statuslists.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package net.openid.conformance.oauth.statuslists; | |
import java.io.ByteArrayOutputStream; | |
import java.util.Base64; | |
import java.util.zip.Deflater; | |
import java.util.zip.Inflater; | |
/** | |
* A wrapper around a compressed status list from the Token Status List (TSL). | |
* See: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-status-list-12 | |
*/ | |
public class TokenStatusList { | |
private final byte[] bytes; | |
private final int bits; | |
public TokenStatusList(byte[] bytes, int bits) { | |
this.bytes = bytes; | |
this.bits = bits; | |
} | |
public static TokenStatusList decode(String encodedStatusList, int bits) { | |
try { | |
return new TokenStatusList(decodeStatusList(encodedStatusList), bits); | |
} catch (Exception e) { | |
throw new IllegalStateException("Could not decompress status list", e); | |
} | |
} | |
public static byte[] decodeStatusList(String encodedStatusList) throws Exception { | |
byte[] compressed = Base64.getUrlDecoder().decode(encodedStatusList); | |
Inflater inflater = new Inflater(); // ZLIB format | |
inflater.setInput(compressed); | |
ByteArrayOutputStream output = new ByteArrayOutputStream(); | |
try { | |
byte[] buffer = new byte[1024]; | |
while (!inflater.finished()) { | |
int count = inflater.inflate(buffer); | |
output.write(buffer, 0, count); | |
} | |
} finally { | |
inflater.end(); | |
} | |
return output.toByteArray(); | |
} | |
public Status getStatus(int idx) { | |
return getStatus(idx, bits); | |
} | |
public Status getStatus(int index, int bitsPerEntry) { | |
int v = getPackedValue(index, bitsPerEntry); | |
return switch (v) { | |
case 0 -> Status.VALID; | |
case 1 -> Status.INVALID; | |
case 2 -> Status.SUSPENDED; | |
case 3 -> Status.STATUS_0X03; | |
default -> throw new IllegalArgumentException("Unknown status code: " + v); | |
}; | |
} | |
/** | |
* LSB-first, entries packed back-to-back. | |
*/ | |
private int getPackedValue(int index, int bitsPerEntry) { | |
if (bitsPerEntry <= 0 || bitsPerEntry > 32) { | |
throw new IllegalArgumentException("bitsPerEntry must be 1..32"); | |
} | |
long mask = (bitsPerEntry == 32) ? 0xFFFF_FFFFL : ((1L << bitsPerEntry) - 1); | |
int bitOffset = index * bitsPerEntry; | |
int byteIndex = bitOffset >>> 3; // / 8 | |
int bitInByte = bitOffset & 7; // % 8 | |
// Build up to 8 bytes into a little-endian 64-bit chunk | |
long chunk = 0; | |
for (int i = 0; i < 8; i++) { | |
int pos = byteIndex + i; | |
if (pos >= bytes.length) { | |
break; | |
} | |
chunk |= ((long) (bytes[pos] & 0xFF)) << (8 * i); | |
} | |
return (int) ((chunk >>> bitInByte) & mask); | |
} | |
public static TokenStatusList create(byte[] rawEntries, int bitsPerEntry) { | |
if (bitsPerEntry <= 0 || bitsPerEntry > 32) { | |
throw new IllegalArgumentException("bitsPerEntry must be 1..32"); | |
} | |
byte[] bytes = packEntries(rawEntries, bitsPerEntry); | |
return new TokenStatusList(bytes, bitsPerEntry); | |
} | |
public String encodeStatusList() { | |
byte[] z = compressZlib(bytes); | |
return Base64.getUrlEncoder().withoutPadding().encodeToString(z); | |
} | |
private static byte[] packEntries(byte[] entries, int bitsPerEntry) { | |
int totalBits = entries.length * bitsPerEntry; | |
byte[] out = new byte[(totalBits + 7) >>> 3]; | |
int maxVal = (bitsPerEntry == 32) ? -1 : (1 << bitsPerEntry); | |
for (int i = 0; i < entries.length; i++) { | |
int v = entries[i] & 0xFF; | |
if (bitsPerEntry < 32 && v >= maxVal) { | |
throw new IllegalArgumentException("entry " + i + " out of range for " + bitsPerEntry + " bits"); | |
} | |
int base = i * bitsPerEntry; | |
for (int b = 0; b < bitsPerEntry; b++) { | |
if (((v >>> b) & 1) == 1) { | |
int bitIndex = base + b; // LSB-first within entry | |
out[bitIndex >>> 3] |= (byte) (1 << (bitIndex & 7)); | |
} | |
} | |
} | |
return out; | |
} | |
private static byte[] compressZlib(byte[] data) { | |
Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION,false); // zlib (nowrap=false) | |
deflater.setInput(data); | |
deflater.finish(); | |
ByteArrayOutputStream baos = new ByteArrayOutputStream(); | |
byte[] buf = new byte[512]; | |
try { | |
while (!deflater.finished()) { | |
int n = deflater.deflate(buf); | |
if (n == 0 && deflater.needsInput()) break; | |
baos.write(buf, 0, n); | |
} | |
} finally { | |
deflater.end(); | |
} | |
return baos.toByteArray(); | |
} | |
/** | |
* See: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-status-list-12#section-7.1 | |
*/ | |
public enum Status { | |
VALID(0x00), | |
INVALID(0x01), | |
SUSPENDED(0x02), | |
// made up from example in https://datatracker.ietf.org/doc/html/draft-ietf-oauth-status-list-12#section-4.1 | |
STATUS_0X03(0x03); | |
private final int typeValue; | |
Status(int typeValue) { | |
this.typeValue = typeValue; | |
} | |
public int getTypeValue() { | |
return typeValue; | |
} | |
public static Status valueOf(byte codetypeValue) { | |
for (Status status : Status.values()) { | |
if (status.typeValue == codetypeValue) { | |
return status; | |
} | |
} | |
throw new IllegalArgumentException("invalid status type value: " + codetypeValue); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package net.openid.conformance.oauth.statuslists; | |
import net.openid.conformance.oauth.statuslists.TokenStatusList.Status; | |
import org.junit.jupiter.api.Test; | |
import static org.junit.jupiter.api.Assertions.assertEquals; | |
public class TokenStatusListTests { | |
@Test | |
public void encodeStatusListWithOneBitEncoding() { | |
// example from spec: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-status-list-12#section-4.1 | |
int bits = 1; | |
byte[] input = new byte[16]; | |
input[0] = 1; | |
input[1] = 0; | |
input[2] = 0; | |
input[3] = 1; | |
input[4] = 1; | |
input[5] = 1; | |
input[6] = 0; | |
input[7] = 1; | |
input[8] = 1; | |
input[9] = 1; | |
input[10] = 0; | |
input[11] = 0; | |
input[12] = 0; | |
input[13] = 1; | |
input[14] = 0; | |
input[15] = 1; | |
TokenStatusList statusList = TokenStatusList.create(input, bits); | |
String encoded = statusList.encodeStatusList(); | |
assertEquals("eNrbuRgAAhcBXQ", encoded); | |
assertEquals(Status.INVALID, statusList.getStatus(0)); | |
assertEquals(Status.VALID, statusList.getStatus(1)); | |
assertEquals(Status.VALID, statusList.getStatus(2)); | |
assertEquals(Status.INVALID, statusList.getStatus(3)); | |
assertEquals(Status.INVALID, statusList.getStatus(4)); | |
assertEquals(Status.INVALID, statusList.getStatus(5)); | |
assertEquals(Status.VALID, statusList.getStatus(6)); | |
assertEquals(Status.INVALID, statusList.getStatus(7)); | |
assertEquals(Status.INVALID, statusList.getStatus(8)); | |
assertEquals(Status.INVALID, statusList.getStatus(9)); | |
assertEquals(Status.VALID, statusList.getStatus(10)); | |
assertEquals(Status.VALID, statusList.getStatus(11)); | |
assertEquals(Status.VALID, statusList.getStatus(12)); | |
assertEquals(Status.INVALID, statusList.getStatus(13)); | |
assertEquals(Status.VALID, statusList.getStatus(14)); | |
assertEquals(Status.INVALID, statusList.getStatus(15)); | |
} | |
@Test | |
public void decodeStatusListWithOneBitEncoding() { | |
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-status-list-12#section-4.2 | |
// example from spec: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-status-list-12#section-4.1 | |
String lst = "eNrbuRgAAhcBXQ"; | |
int bits = 1; | |
TokenStatusList statusList = TokenStatusList.decode(lst, bits); | |
/* | |
* status[0] = 1 | |
* status[1] = 0 | |
* status[2] = 0 | |
* status[3] = 1 | |
* status[4] = 1 | |
* status[5] = 1 | |
* status[6] = 0 | |
* status[7] = 1 | |
* status[8] = 1 | |
* status[9] = 1 | |
* status[10] = 0 | |
* status[11] = 0 | |
* status[12] = 0 | |
* status[13] = 1 | |
* status[14] = 0 | |
* status[15] = 1 | |
*/ | |
assertEquals(Status.INVALID, statusList.getStatus(0)); | |
assertEquals(Status.VALID, statusList.getStatus(1)); | |
assertEquals(Status.VALID, statusList.getStatus(2)); | |
assertEquals(Status.INVALID, statusList.getStatus(3)); | |
assertEquals(Status.INVALID, statusList.getStatus(4)); | |
assertEquals(Status.INVALID, statusList.getStatus(5)); | |
assertEquals(Status.VALID, statusList.getStatus(6)); | |
assertEquals(Status.INVALID, statusList.getStatus(7)); | |
assertEquals(Status.INVALID, statusList.getStatus(8)); | |
assertEquals(Status.INVALID, statusList.getStatus(9)); | |
assertEquals(Status.VALID, statusList.getStatus(10)); | |
assertEquals(Status.VALID, statusList.getStatus(11)); | |
assertEquals(Status.VALID, statusList.getStatus(12)); | |
assertEquals(Status.INVALID, statusList.getStatus(13)); | |
assertEquals(Status.VALID, statusList.getStatus(14)); | |
assertEquals(Status.INVALID, statusList.getStatus(15)); | |
} | |
@Test | |
public void decodeStatusListWithTwoBitEncoding() { | |
// example from spec: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-status-list-12#section-4.2 | |
String lst = "eNo76fITAAPfAgc"; | |
int bits = 2; | |
TokenStatusList statusList = TokenStatusList.decode(lst, bits); | |
/* | |
* status[0] = 1 | |
* status[1] = 2 | |
* status[2] = 0 | |
* status[3] = 3 | |
* status[4] = 0 | |
* status[5] = 1 | |
* status[6] = 0 | |
* status[7] = 1 | |
* status[8] = 1 | |
* status[9] = 2 | |
* status[10] = 3 | |
* status[11] = 3 | |
*/ | |
assertEquals(Status.INVALID, statusList.getStatus(0)); | |
assertEquals(Status.SUSPENDED, statusList.getStatus(1)); | |
assertEquals(Status.VALID, statusList.getStatus(2)); | |
assertEquals(Status.STATUS_0X03, statusList.getStatus(3)); | |
assertEquals(Status.VALID, statusList.getStatus(4)); | |
assertEquals(Status.INVALID, statusList.getStatus(5)); | |
assertEquals(Status.VALID, statusList.getStatus(6)); | |
assertEquals(Status.INVALID, statusList.getStatus(7)); | |
assertEquals(Status.INVALID, statusList.getStatus(8)); | |
assertEquals(Status.SUSPENDED, statusList.getStatus(9)); | |
assertEquals(Status.STATUS_0X03, statusList.getStatus(10)); | |
assertEquals(Status.STATUS_0X03, statusList.getStatus(11)); | |
} | |
@Test | |
public void decodeStatusListWithOneBitEncodingLarge() { | |
// 1-bit test vector example from spec: | |
// see: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-status-list-12#autoid-78 | |
String lst = "eNrt3AENwCAMAEGogklACtKQPg9LugC9k_ACvreiogE" + | |
"AAKkeCQAAAAAAAAAAAAAAAAAAAIBylgQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + | |
"AAAAAAAAAAAAAAAAAAAXG9IAAAAAAAAAPwsJAAAAAAAAAAAAAAAvhsSAAAAAAAAAAA" + | |
"A7KpLAAAAAAAAAAAAAAAAAAAAAJsLCQAAAAAAAAAAADjelAAAAAAAAAAAKjDMAQAAA" + | |
"ACAZC8L2AEb"; | |
int bits = 1; | |
TokenStatusList statusList = TokenStatusList.decode(lst, bits); | |
/* | |
* status[0]=1 | |
* status[1993]=1 | |
* status[25460]=1 | |
* status[159495]=1 | |
* status[495669]=1 | |
* status[554353]=1 | |
* status[645645]=1 | |
* status[723232]=1 | |
* status[854545]=1 | |
* status[934534]=1 | |
* status[1000345]=1 | |
*/ | |
assertEquals(Status.INVALID, statusList.getStatus(0)); | |
assertEquals(Status.VALID, statusList.getStatus(1)); | |
assertEquals(Status.VALID, statusList.getStatus(2)); | |
assertEquals(Status.VALID, statusList.getStatus(1992)); | |
assertEquals(Status.INVALID, statusList.getStatus(1993)); | |
assertEquals(Status.VALID, statusList.getStatus(1994)); | |
assertEquals(Status.INVALID, statusList.getStatus(25460)); | |
assertEquals(Status.INVALID, statusList.getStatus(159495)); | |
assertEquals(Status.VALID, statusList.getStatus(1000344)); | |
assertEquals(Status.INVALID, statusList.getStatus(1000345)); | |
} | |
@Test | |
public void decodeStatusListWithTwoBitEncodingLarge() { | |
// 2-bit test vector example from spec | |
// See: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-status-list-12#autoid-79 | |
String lst = "eNrt2zENACEQAEEuoaBABP5VIO01fCjIHTMStt9ovGV" + | |
"IAAAAAABAbiEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEB5WwIAAAAAA" + | |
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + | |
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAID0ugQAAAAAAAAAAAAAAAAAQG12SgAAA" + | |
"AAAAAAAAAAAAAAAAAAAAAAAAOCSIQEAAAAAAAAAAAAAAAAAAAAAAAD8ExIAAAAAAAA" + | |
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwJEuAQAAAAAAAAAAAAAAAAAAAAAAAMB9S" + | |
"wIAAAAAAAAAAAAAAAAAAACoYUoAAAAAAAAAAAAAAEBqH81gAQw"; | |
int bits = 2; | |
TokenStatusList statusList = TokenStatusList.decode(lst, bits); | |
/* | |
* status[0]=1 | |
* status[1993]=2 | |
* status[25460]=1 | |
* status[159495]=3 | |
* status[495669]=1 | |
* status[554353]=1 | |
* status[645645]=2 | |
* status[723232]=1 | |
* status[854545]=1 | |
* status[934534]=2 | |
* status[1000345]=3 | |
*/ | |
assertEquals(Status.INVALID, statusList.getStatus(0)); | |
assertEquals(Status.VALID, statusList.getStatus(1)); | |
assertEquals(Status.VALID, statusList.getStatus(2)); | |
assertEquals(Status.VALID, statusList.getStatus(1992)); | |
assertEquals(Status.SUSPENDED, statusList.getStatus(1993)); | |
assertEquals(Status.VALID, statusList.getStatus(1994)); | |
assertEquals(Status.INVALID, statusList.getStatus(25460)); | |
assertEquals(Status.STATUS_0X03, statusList.getStatus(159495)); | |
assertEquals(Status.INVALID, statusList.getStatus(495669)); | |
assertEquals(Status.INVALID, statusList.getStatus(554353)); | |
assertEquals(Status.SUSPENDED, statusList.getStatus(645645)); | |
assertEquals(Status.INVALID, statusList.getStatus(723232)); | |
assertEquals(Status.INVALID, statusList.getStatus(854545)); | |
assertEquals(Status.SUSPENDED, statusList.getStatus(934534)); | |
assertEquals(Status.STATUS_0X03, statusList.getStatus(1000345)); | |
assertEquals(Status.VALID, statusList.getStatus(1000346)); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment