Repository: jwtk/jjwt (java)

Issue:
java.lang.NoSuchMethodError: org.json.JSONTokener.<init>(Ljava/io/Reader;)V
**Description**
JWS cannot be deserialized in Android devices.

**Steps to reproduce**
1. Add the required dependencies for an Android project as shown in the README.md
```
dependencies {
    api('io.jsonwebtoken:jjwt-api:0.12.3')
    runtimeOnly('io.jsonwebtoken:jjwt-impl:0.12.3') 
    runtimeOnly('io.jsonwebtoken:jjwt-orgjson:0.12.3') {
        exclude(group: 'org.json', module: 'json') //provided by Android natively
    }
}
```
2. Create a parser
```
Jwts.parser()
  .verifyWith(publicKey)
  .build()
  .parseSignedClaims(jwsString);
```
3. Try to deserialize a signed JWT in an Android device
4. An error is thrown: `java.lang.NoSuchMethodError: org.json.JSONTokener.<init>(Ljava/io/Reader;)V`

**Cause**
Since 0.12.0, the class `io.jsonwebtoken.orgjson.io.OrgJsonDeserializer`, in its `parse()` method, is trying to create a `JSONTokener` passing a `Reader` as argument to the constructor.
This constructor is not available in the Android implementation of `JSONTokener`: https://android.googlesource.com/platform/libcore/+/refs/heads/main/json/src/main/java/org/json/JSONTokener.java





Pull request:
JSONTokener constructor fallback
OrgJsonDeserializer: Added fallback implementation for Android when `JSONTokener(Reader)` constructor is not available.

Closes #882

Patch:
diff --git a/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonDeserializer.java b/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonDeserializer.java
index 49be067..0367eeb 100644
--- a/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonDeserializer.java
+++ b/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonDeserializer.java
@@ -16,11 +16,14 @@
 package io.jsonwebtoken.orgjson.io;
 
 import io.jsonwebtoken.io.AbstractDeserializer;
+import io.jsonwebtoken.lang.Assert;
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.json.JSONTokener;
 
+import java.io.CharArrayReader;
+import java.io.IOException;
 import java.io.Reader;
 import java.util.ArrayList;
 import java.util.Iterator;
@@ -33,14 +36,25 @@ import java.util.Map;
  */
 public class OrgJsonDeserializer extends AbstractDeserializer<Object> {
 
+    private final JSONTokenerFactory TOKENER_FACTORY;
+
+    public OrgJsonDeserializer() {
+        this(JSONTokenerFactory.INSTANCE);
+    }
+
+    private OrgJsonDeserializer(JSONTokenerFactory factory) {
+        this.TOKENER_FACTORY = Assert.notNull(factory, "JSONTokenerFactory cannot be null.");
+    }
+
     @Override
     protected Object doDeserialize(Reader reader) {
         return parse(reader);
     }
 
-    private Object parse(java.io.Reader reader) throws JSONException {
+    private Object parse(Reader reader) throws JSONException {
 
-        JSONTokener tokener = new JSONTokener(reader);
+        JSONTokener tokener = this.TOKENER_FACTORY.newTokener(reader);
+        Assert.notNull(tokener, "JSONTokener cannot be null.");
 
         char c = tokener.nextClean(); //peak ahead
         tokener.back(); //revert
@@ -94,4 +108,67 @@ public class OrgJsonDeserializer extends AbstractDeserializer<Object> {
         }
         return value;
     }
+
+    /**
+     * A factory to create {@link JSONTokener} instances from {@link Reader}s.
+     *
+     * @see <a href="https://github.com/jwtk/jjwt/issues/882">JJWT Issue 882</a>.
+     * @since 0.12.4
+     */
+    static class JSONTokenerFactory { // package-protected on purpose. Not to be exposed as part of public API
+
+        private static final Reader TEST_READER = new CharArrayReader("{}".toCharArray());
+
+        private static final JSONTokenerFactory INSTANCE = new JSONTokenerFactory();
+
+        private final boolean readerCtorAvailable;
+
+        // package protected visibility for testing only:
+        JSONTokenerFactory() {
+            boolean avail = true;
+            try {
+                testTokener(TEST_READER);
+            } catch (NoSuchMethodError err) {
+                avail = false;
+            }
+            this.readerCtorAvailable = avail;
+        }
+
+        // visible for testing only
+        protected void testTokener(@SuppressWarnings("SameParameterValue") Reader reader) throws NoSuchMethodError {
+            new JSONTokener(reader);
+        }
+
+        /**
+         * Reads all content from the specified reader and returns it as a single String.
+         *
+         * @param reader the reader to read characters from
+         * @return the reader content as a single string
+         */
+        private static String toString(Reader reader) throws IOException {
+            StringBuilder sb = new StringBuilder(4096);
+            char[] buf = new char[4096];
+            int n = 0;
+            while (EOF != n) {
+                n = reader.read(buf);
+                if (n > 0) sb.append(buf, 0, n);
+            }
+            return sb.toString();
+        }
+
+        private JSONTokener newTokener(Reader reader) {
+            if (this.readerCtorAvailable) {
+                return new JSONTokener(reader);
+            }
+            // otherwise not available, likely Android or earlier org.json version, fall back to String ctor:
+            String s;
+            try {
+                s = toString(reader);
+            } catch (IOException ex) {
+                String msg = "Unable to obtain JSON String from Reader: " + ex.getMessage();
+                throw new JSONException(msg, ex);
+            }
+            return new JSONTokener(s);
+        }
+    }
 }


Test patch:
diff --git a/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonDeserializerTest.groovy b/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonDeserializerTest.groovy
index 5f27dcb..df780b0 100644
--- a/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonDeserializerTest.groovy
+++ b/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonDeserializerTest.groovy
@@ -18,6 +18,7 @@ package io.jsonwebtoken.orgjson.io
 
 import io.jsonwebtoken.io.DeserializationException
 import io.jsonwebtoken.io.Deserializer
+import io.jsonwebtoken.io.IOException
 import io.jsonwebtoken.lang.Strings
 import org.junit.Before
 import org.junit.Test
@@ -28,9 +29,17 @@ class OrgJsonDeserializerTest {
 
     private OrgJsonDeserializer des
 
-    private Object fromBytes(byte[] data) {
+    private static Reader reader(byte[] data) {
         def ins = new ByteArrayInputStream(data)
-        def reader = new InputStreamReader(ins, Strings.UTF_8)
+        return new InputStreamReader(ins, Strings.UTF_8)
+    }
+
+    private static Reader reader(String s) {
+        return reader(Strings.utf8(s))
+    }
+
+    private Object fromBytes(byte[] data) {
+        def reader = reader(data)
         return des.deserialize(reader)
     }
 
@@ -188,4 +197,64 @@ class OrgJsonDeserializerTest {
         }
     }
 
+    /**
+     * Asserts that, when the JSONTokener(Reader) constructor isn't available (e.g. on Android), that the Reader is
+     * converted to a String and the JSONTokener(String) constructor is used instead.
+     * @since 0.12.4
+     */
+    @Test
+    void jsonTokenerMissingReaderConstructor() {
+
+        def json = '{"hello": "世界", "test": [1, 2]}'
+        def expected = read(json) // 'normal' reading
+
+        des = new OrgJsonDeserializer(new NoReaderCtorTokenerFactory())
+
+        def reader = reader('{"hello": "世界", "test": [1, 2]}')
+
+        def result = des.deserialize(reader) // should still work
+
+        assertEquals expected, result
+    }
+
+    /**
+     * Asserts that, when the JSONTokener(Reader) constructor isn't available, and conversion of the Reader to a String
+     * fails, that a JSONException is thrown
+     * @since 0.12.4
+     */
+    @Test
+    void readerFallbackToStringFails() {
+        def causeMsg = 'Reader failed.'
+        def cause = new java.io.IOException(causeMsg)
+        def reader = new Reader() {
+            @Override
+            int read(char[] cbuf, int off, int len) throws IOException {
+                throw cause
+            }
+
+            @Override
+            void close() throws IOException {
+            }
+        }
+
+        des = new OrgJsonDeserializer(new NoReaderCtorTokenerFactory())
+
+        try {
+            des.deserialize(reader)
+            fail()
+        } catch (DeserializationException expected) {
+            def jsonEx = expected.getCause()
+            String msg = "Unable to obtain JSON String from Reader: $causeMsg"
+            assertEquals msg, jsonEx.getMessage()
+            assertSame cause, jsonEx.getCause()
+        }
+    }
+
+    private static class NoReaderCtorTokenerFactory extends OrgJsonDeserializer.JSONTokenerFactory {
+        @Override
+        protected void testTokener(Reader reader) throws NoSuchMethodError {
+            throw new NoSuchMethodError('Android says nope!')
+        }
+    }
+
 }