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.

Revisions

  1. thomasdarimont revised this gist Oct 9, 2025. 2 changed files with 234 additions and 121 deletions.
    177 changes: 177 additions & 0 deletions TokenStatusList.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,177 @@
    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);
    }
    }

    }
    178 changes: 57 additions & 121 deletions StatusListTests.java → TokenStatusListTests.java
    Original file line number Diff line number Diff line change
    @@ -1,12 +1,57 @@
    import org.junit.jupiter.api.Test;
    package net.openid.conformance.oauth.statuslists;

    import java.io.ByteArrayOutputStream;
    import java.util.Base64;
    import java.util.zip.Inflater;
    import net.openid.conformance.oauth.statuslists.TokenStatusList.Status;
    import org.junit.jupiter.api.Test;

    import static org.junit.jupiter.api.Assertions.assertEquals;

    public class StatusListTests {
    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() {
    @@ -16,7 +61,7 @@ public void decodeStatusListWithOneBitEncoding() {
    // example from spec: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-status-list-12#section-4.1
    String lst = "eNrbuRgAAhcBXQ";
    int bits = 1;
    StatusList statusList = StatusList.decode(lst, bits);
    TokenStatusList statusList = TokenStatusList.decode(lst, bits);

    /*
    * status[0] = 1
    @@ -60,9 +105,9 @@ 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;
    StatusList statusList = StatusList.decode(lst, bits);
    TokenStatusList statusList = TokenStatusList.decode(lst, bits);

    /**
    /*
    * status[0] = 1
    * status[1] = 2
    * status[2] = 0
    @@ -101,9 +146,9 @@ public void decodeStatusListWithOneBitEncodingLarge() {
    "A7KpLAAAAAAAAAAAAAAAAAAAAAJsLCQAAAAAAAAAAADjelAAAAAAAAAAAKjDMAQAAA" +
    "ACAZC8L2AEb";
    int bits = 1;
    StatusList statusList = StatusList.decode(lst, bits);
    TokenStatusList statusList = TokenStatusList.decode(lst, bits);

    /**
    /*
    * status[0]=1
    * status[1993]=1
    * status[25460]=1
    @@ -141,9 +186,9 @@ public void decodeStatusListWithTwoBitEncodingLarge() {
    "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwJEuAQAAAAAAAAAAAAAAAAAAAAAAAMB9S" +
    "wIAAAAAAAAAAAAAAAAAAACoYUoAAAAAAAAAAAAAAEBqH81gAQw";
    int bits = 2;
    StatusList statusList = StatusList.decode(lst, bits);
    TokenStatusList statusList = TokenStatusList.decode(lst, bits);

    /**
    /*
    * status[0]=1
    * status[1993]=2
    * status[25460]=1
    @@ -174,113 +219,4 @@ public void decodeStatusListWithTwoBitEncodingLarge() {
    assertEquals(Status.VALID, statusList.getStatus(1000346));
    }

    public static class StatusList {

    private final byte[] bytes;
    private final int bits;

    public StatusList(byte[] bytes, int bits) {
    this.bytes = bytes;
    this.bits = bits;
    }

    public static StatusList decode(String encodedStatusList, int bits) {
    try {
    return new StatusList(decompressStatusList(encodedStatusList), bits);
    } catch (Exception e) {
    throw new IllegalStateException("Could not decompress status list", e);
    }
    }

    public static byte[] decompressStatusList(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);
    }

    }

    /**
    * 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 static Status valueOf(byte codetypeValue) {
    for (Status status : Status.values()) {
    if (status.typeValue == codetypeValue) {
    return status;
    }
    }
    throw new IllegalArgumentException("invalid status type value: " + codetypeValue);
    }
    }
    }
  2. thomasdarimont revised this gist Oct 9, 2025. 1 changed file with 183 additions and 22 deletions.
    205 changes: 183 additions & 22 deletions StatusListTests.java
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,3 @@

    import org.junit.jupiter.api.Test;

    import java.io.ByteArrayOutputStream;
    @@ -16,9 +15,8 @@ public void decodeStatusListWithOneBitEncoding() {

    // example from spec: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-status-list-12#section-4.1
    String lst = "eNrbuRgAAhcBXQ";
    System.out.println(lst);

    StatusList statusList = StatusList.decode(lst);
    int bits = 1;
    StatusList statusList = StatusList.decode(lst, bits);

    /*
    * status[0] = 1
    @@ -41,22 +39,154 @@ public void decodeStatusListWithOneBitEncoding() {
    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(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;
    StatusList statusList = StatusList.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;
    StatusList statusList = StatusList.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;
    StatusList statusList = StatusList.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));
    }

    public static class StatusList {

    private final byte[] bytes;
    private final int bits;

    public StatusList(byte[] bytes) {
    public StatusList(byte[] bytes, int bits) {
    this.bytes = bytes;
    this.bits = bits;
    }

    public static StatusList decode(String encodedStatusList) {
    public static StatusList decode(String encodedStatusList, int bits) {
    try {
    return new StatusList(decompressStatusList(encodedStatusList));
    return new StatusList(decompressStatusList(encodedStatusList), bits);
    } catch (Exception e) {
    throw new IllegalStateException("Could not decompress status list", e);
    }
    @@ -69,29 +199,57 @@ public static byte[] decompressStatusList(String encodedStatusList) throws Excep
    Inflater inflater = new Inflater(); // ZLIB format
    inflater.setInput(compressed);

    byte[] buffer = new byte[1024];
    ByteArrayOutputStream output = new ByteArrayOutputStream();

    while (!inflater.finished()) {
    int count = inflater.inflate(buffer);
    output.write(buffer, 0, count);
    try {
    byte[] buffer = new byte[1024];
    while (!inflater.finished()) {
    int count = inflater.inflate(buffer);
    output.write(buffer, 0, count);
    }
    } finally {
    inflater.end();
    }

    inflater.end();
    return output.toByteArray();
    }

    public Status getStatus(int idx) {
    return getStatus(idx, bytes);
    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);
    };
    }

    private Status getStatus(int idx, byte[] bytes) {
    if (idx >= bytes.length) {
    throw new IllegalArgumentException("Invalid status list entry at index " + idx);
    /**
    * 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

    byte statusBytes = bytes[idx];
    return Status.valueOf(statusBytes);
    // 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);
    }

    }
    @@ -105,7 +263,10 @@ public enum Status {

    INVALID(0x01),

    SUSPENDED(0x02);
    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;

  3. thomasdarimont created this gist Oct 9, 2025.
    125 changes: 125 additions & 0 deletions StatusListTests.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,125 @@

    import org.junit.jupiter.api.Test;

    import java.io.ByteArrayOutputStream;
    import java.util.Base64;
    import java.util.zip.Inflater;

    import static org.junit.jupiter.api.Assertions.assertEquals;

    public class StatusListTests {

    @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";
    System.out.println(lst);

    StatusList statusList = StatusList.decode(lst);

    /*
    * 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(4));
    assertEquals(Status.INVALID, statusList.getStatus(5));
    assertEquals(Status.VALID, statusList.getStatus(5));
    }

    public static class StatusList {

    private final byte[] bytes;

    public StatusList(byte[] bytes) {
    this.bytes = bytes;
    }

    public static StatusList decode(String encodedStatusList) {
    try {
    return new StatusList(decompressStatusList(encodedStatusList));
    } catch (Exception e) {
    throw new IllegalStateException("Could not decompress status list", e);
    }
    }

    public static byte[] decompressStatusList(String encodedStatusList) throws Exception {

    byte[] compressed = Base64.getUrlDecoder().decode(encodedStatusList);

    Inflater inflater = new Inflater(); // ZLIB format
    inflater.setInput(compressed);

    byte[] buffer = new byte[1024];
    ByteArrayOutputStream output = new ByteArrayOutputStream();

    while (!inflater.finished()) {
    int count = inflater.inflate(buffer);
    output.write(buffer, 0, count);
    }

    inflater.end();
    return output.toByteArray();
    }

    public Status getStatus(int idx) {
    return getStatus(idx, bytes);
    }

    private Status getStatus(int idx, byte[] bytes) {
    if (idx >= bytes.length) {
    throw new IllegalArgumentException("Invalid status list entry at index " + idx);
    }

    byte statusBytes = bytes[idx];
    return Status.valueOf(statusBytes);
    }

    }

    /**
    * 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);

    private final int typeValue;

    Status(int typeValue) {
    this.typeValue = 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);
    }
    }
    }