Last active
October 9, 2025 14:04
-
-
Save thomasdarimont/709de42f09598d210fbfa9cdad9f4d3f to your computer and use it in GitHub Desktop.
Revisions
-
thomasdarimont revised this gist
Oct 9, 2025 . 2 changed files with 234 additions and 121 deletions.There are no files selected for viewing
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 charactersOriginal 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); } } } 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 charactersOriginal file line number Diff line number Diff line change @@ -1,12 +1,57 @@ 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() { @@ -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; 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; 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; 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; 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)); } } -
thomasdarimont revised this gist
Oct 9, 2025 . 1 changed file with 183 additions and 22 deletions.There are no files selected for viewing
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 charactersOriginal 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"; 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(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, 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); } @@ -69,29 +199,57 @@ public static byte[] decompressStatusList(String encodedStatusList) throws Excep 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); } } @@ -105,7 +263,10 @@ public enum Status { 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; -
thomasdarimont created this gist
Oct 9, 2025 .There are no files selected for viewing
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 charactersOriginal 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); } } }