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<T>)` 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.
This commit is contained in:
sainaen 2016-09-20 12:14:24 +03:00
parent 8966c3a912
commit 13906d3746
3 changed files with 141 additions and 3 deletions

View File

@ -120,10 +120,25 @@ public class DefaultClaims extends JwtMap implements Claims {
value = getDate(claimName);
}
return castClaimValue(value, requiredType);
}
private <T> T castClaimValue(Object value, Class<T> 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());
}

View File

@ -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<Header,Claims> 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.
// ========================================================================

View File

@ -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