From 13906d3746d29e9610a0f3c385dbb14932fe9d71 Mon Sep 17 00:00:00 2001 From: sainaen Date: Tue, 20 Sep 2016 12:14:24 +0300 Subject: [PATCH] Implement type conversions of integral claim values Jackson chooses the target type for JSON numbers based on their value, while deserializing without correct typing information present. This leads to a confusing behavior: String token = Jwts.builder() .claim("byte", (byte) 42) .claim("short", (short) 42) .claim("int", 42) .claim("long_small", (long) 42) .claim("long_big", ((long) Integer.MAX_VALUE) + 42) .compact(); Claims claims = (Claims) Jwts.parser().parse(token).getBody(); claims.get("int", Integer.class); // => 42 claims.get("long_big", Long.class); // => ((long) Integer.MAX_VALUE) + 42 claims.get("long_small", Long.class); // throws RequiredTypeException: required=Long, found=Integer claims.get("short", Short.class); // throws RequiredTypeException: required=Short, found=Integer claims.get("byte", Byte.class); // throws RequiredTypeException: required=Byte, found=Integer With this commit, `DefaultClaims.getClaim(String, Class)` will correctly handle cases when required type is `Long`, `Integer`, `Short` or `Byte`: check that value fits in the required type and cast to it. // ... setup is the same as above claims.get("int", Integer.class); // => 42 claims.get("long_big", Long.class); // => ((long) Integer.MAX_VALUE) + 42 claims.get("long_small", Long.class); // => (long) 42 claims.get("short", Short.class); // => (short) 42 claims.get("byte", Byte.class); // => (byte) 42 Fixes #142. --- .../io/jsonwebtoken/impl/DefaultClaims.java | 15 +++ .../io/jsonwebtoken/JwtParserTest.groovy | 31 ++++++ .../impl/DefaultClaimsTest.groovy | 98 ++++++++++++++++++- 3 files changed, 141 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java b/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java index 196c82ff..2523d305 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java @@ -120,10 +120,25 @@ public class DefaultClaims extends JwtMap implements Claims { value = getDate(claimName); } + return castClaimValue(value, requiredType); + } + + private T castClaimValue(Object value, Class requiredType) { if (requiredType == Date.class && value instanceof Long) { value = new Date((Long)value); } + if (value instanceof Integer) { + int intValue = (Integer) value; + if (requiredType == Long.class) { + value = (long) intValue; + } else if (requiredType == Short.class && Short.MIN_VALUE <= intValue && intValue <= Short.MAX_VALUE) { + value = (short) intValue; + } else if (requiredType == Byte.class && Byte.MIN_VALUE <= intValue && intValue <= Byte.MAX_VALUE) { + value = (byte) intValue; + } + } + if (!requiredType.isInstance(value)) { throw new RequiredTypeException("Expected value to be of type: " + requiredType + ", but was " + value.getClass()); } diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index f3a0368d..187711fe 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -716,6 +716,37 @@ class JwtParserTest { } } + @Test + void testParseClaimsJwsWithNumericTypes() { + byte[] key = randomKey() + + def b = (byte) 42 + def s = (short) 42 + def i = 42 + + def smallLong = (long) 42 + def bigLong = ((long) Integer.MAX_VALUE) + 42 + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + claim("byte", b). + claim("short", s). + claim("int", i). + claim("long_small", smallLong). + claim("long_big", bigLong). + compact() + + Jwt jwt = Jwts.parser().setSigningKey(key).parseClaimsJws(compact) + + Claims claims = jwt.getBody() + + assertEquals(b, claims.get("byte", Byte.class)) + assertEquals(s, claims.get("short", Short.class)) + assertEquals(i, claims.get("int", Integer.class)) + assertEquals(smallLong, claims.get("long_small", Long.class)) + assertEquals(bigLong, claims.get("long_big", Long.class)) + } + + // ======================================================================== // parsePlaintextJws with signingKey resolver. // ======================================================================== diff --git a/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy index 161957f0..4d642322 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy @@ -52,11 +52,103 @@ class DefaultClaimsTest { } @Test - void testGetClaimWithRequiredType_Success() { - claims.put("anInteger", new Integer(5)) + void testGetClaimWithRequiredType_Integer_Success() { + def expected = new Integer(5) + claims.put("anInteger", expected) Object result = claims.get("anInteger", Integer.class) + assertEquals(expected, result) + } - assertTrue(result instanceof Integer) + @Test + void testGetClaimWithRequiredType_Long_Success() { + def expected = new Long(123) + claims.put("aLong", expected) + Object result = claims.get("aLong", Long.class) + assertEquals(expected, result) + } + + @Test + void testGetClaimWithRequiredType_LongWithInteger_Success() { + // long value that fits inside an Integer + def expected = new Long(Integer.MAX_VALUE - 100) + // deserialized as an Integer from JSON + // (type information is not available during parsing) + claims.put("smallLong", expected.intValue()) + // should still be available as Long + Object result = claims.get("smallLong", Long.class) + assertEquals(expected, result) + } + + @Test + void testGetClaimWithRequiredType_ShortWithInteger_Success() { + def expected = new Short((short) 42) + claims.put("short", expected.intValue()) + Object result = claims.get("short", Short.class) + assertEquals(expected, result) + } + + @Test + void testGetClaimWithRequiredType_ShortWithBigInteger_Exception() { + claims.put("tooBigForShort", ((int) Short.MAX_VALUE) + 42) + try { + claims.get("tooBigForShort", Short.class) + fail("getClaim() shouldn't silently lose precision.") + } catch (RequiredTypeException e) { + assertEquals( + e.getMessage(), + "Expected value to be of type: class java.lang.Short, but was class java.lang.Integer" + ) + } + } + + @Test + void testGetClaimWithRequiredType_ShortWithSmallInteger_Exception() { + claims.put("tooSmallForShort", ((int) Short.MIN_VALUE) - 42) + try { + claims.get("tooSmallForShort", Short.class) + fail("getClaim() shouldn't silently lose precision.") + } catch (RequiredTypeException e) { + assertEquals( + e.getMessage(), + "Expected value to be of type: class java.lang.Short, but was class java.lang.Integer" + ) + } + } + + @Test + void testGetClaimWithRequiredType_ByteWithInteger_Success() { + def expected = new Byte((byte) 42) + claims.put("byte", expected.intValue()) + Object result = claims.get("byte", Byte.class) + assertEquals(expected, result) + } + + @Test + void testGetClaimWithRequiredType_ByteWithBigInteger_Exception() { + claims.put("tooBigForByte", ((int) Byte.MAX_VALUE) + 42) + try { + claims.get("tooBigForByte", Byte.class) + fail("getClaim() shouldn't silently lose precision.") + } catch (RequiredTypeException e) { + assertEquals( + e.getMessage(), + "Expected value to be of type: class java.lang.Byte, but was class java.lang.Integer" + ) + } + } + + @Test + void testGetClaimWithRequiredType_ByteWithSmallInteger_Exception() { + claims.put("tooSmallForByte", ((int) Byte.MIN_VALUE) - 42) + try { + claims.get("tooSmallForByte", Byte.class) + fail("getClaim() shouldn't silently lose precision.") + } catch (RequiredTypeException e) { + assertEquals( + e.getMessage(), + "Expected value to be of type: class java.lang.Byte, but was class java.lang.Integer" + ) + } } @Test