Skip to content

Instantly share code, notes, and snippets.

@thomasdarimont
Last active October 9, 2025 14:04
Show Gist options
  • Save thomasdarimont/709de42f09598d210fbfa9cdad9f4d3f to your computer and use it in GitHub Desktop.
Save thomasdarimont/709de42f09598d210fbfa9cdad9f4d3f to your computer and use it in GitHub Desktop.
Initial tests for working with Token Status List (TSL) compressed statuslists.
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);
}
}
}
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