335: initial pluggable JSON (de)serialization support with Jackson and org.json as the first implementations, with Jackson being the default. Added tests to retain 100% code coverage.

This commit is contained in:
Les Hazlewood 2018-07-09 16:57:56 -04:00
parent bae78f03f4
commit 8afca0d0df
33 changed files with 1797 additions and 213 deletions

27
pom.xml
View File

@ -86,6 +86,7 @@
<buildNumber>${user.name}-${maven.build.timestamp}</buildNumber>
<jackson.version>2.9.6</jackson.version>
<orgjson.version>20180130</orgjson.version>
<!-- Optional Runtime Dependencies: -->
<bouncycastle.version>1.56</bouncycastle.version>
@ -98,18 +99,27 @@
<powermock.version>2.0.0-beta.5</powermock.version> <!-- necessary for Java 9 support -->
<failsafe.plugin.version>2.22.0</failsafe.plugin.version>
<surefire.plugin.version>2.22.0</surefire.plugin.version>
<clover.version>4.2.0</clover.version>
<clover.version>4.2.1</clover.version>
</properties>
<dependencies>
<!-- Optional Dependencies: -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
<scope>compile</scope>
<!-- TODO: make optional after project is broken up into targeted artifacts -->
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>${orgjson.version}</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
<!-- Optional Dependencies: -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
@ -295,14 +305,13 @@
<version>${clover.version}</version>
<configuration>
<excludes>
<exclude>**/*Test*</exclude>
<!-- leaving out lang as it mostly comes from other sources -->
<exclude>io/jsonwebtoken/lang/*</exclude>
</excludes>
<methodPercentage>100%</methodPercentage>
<statementPercentage>100%</statementPercentage>
<conditionalPercentage>100%</conditionalPercentage>
<targetPercentage>100%</targetPercentage>
<methodPercentage>100.000000%</methodPercentage>
<statementPercentage>100.000000%</statementPercentage>
<conditionalPercentage>100.000000%</conditionalPercentage>
<targetPercentage>100.000000%</targetPercentage>
</configuration>
<executions>
<execution>
@ -310,8 +319,8 @@
<phase>test</phase>
<goals>
<goal>instrument</goal>
<goal>check</goal>
<goal>clover</goal>
<goal>check</goal>
</goals>
</execution>
</executions>

View File

@ -16,6 +16,7 @@
package io.jsonwebtoken;
import io.jsonwebtoken.codec.Encoder;
import io.jsonwebtoken.io.Serializer;
import java.security.Key;
import java.util.Date;
@ -426,6 +427,20 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
*/
JwtBuilder base64UrlEncodeWith(Encoder<byte[], String> base64UrlEncoder);
/**
* Performs object-to-JSON serialization with the specified Serializer. This is used by the builder to convert
* JWT/JWS/JWT headers and claims Maps to JSON strings as required by the JWT specification.
*
* <p>If this method is not called, JJWT will use whatever serializer it can find at runtime, checking for the
* presence of well-known implementations such Jackson, Gson, and org.json. If one of these is not found
* in the runtime classpath, an exception will be thrown when the {@link #compact()} method is invoked.</p>
*
* @param serializer the serializer to use when converting Map objects to JSON strings.
* @return the builder for method chaining.
* @since 0.10.0
*/
JwtBuilder serializeToJsonWith(Serializer<Map<String,?>> serializer);
/**
* Actually builds the JWT and serializes it to a compact, URL-safe string according to the
* <a href="https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25#section-7">JWT Compact Serialization</a>

View File

@ -17,9 +17,11 @@ package io.jsonwebtoken;
import io.jsonwebtoken.codec.Decoder;
import io.jsonwebtoken.impl.DefaultClock;
import io.jsonwebtoken.io.Deserializer;
import java.security.Key;
import java.util.Date;
import java.util.Map;
/**
* A parser for reading JWT strings, used to convert them into a {@link Jwt} object representing the expanded JWT.
@ -282,6 +284,22 @@ public interface JwtParser {
*/
JwtParser base64UrlDecodeWith(Decoder<String, byte[]> base64UrlDecoder);
/**
* Uses the specified deserializer to convert JSON Strings (UTF-8 byte arrays) into Java Map objects. This is
* used by the parser after Base64Url-decoding to convert JWT/JWS/JWT JSON headers and claims into Java Map
* objects.
*
* <p>If this method is not called, JJWT will use whatever deserializer it can find at runtime, checking for the
* presence of well-known implementations such Jackson, Gson, and org.json. If one of these is not found
* in the runtime classpath, an exception will be thrown when one of the various {@code parse}* methods is
* invoked.</p>
*
* @param deserializer the deserializer to use when converting JSON Strings (UTF-8 byte arrays) into Map objects.
* @return the builder for method chaining.
* @since 0.10.0
*/
JwtParser deserializeJsonWith(Deserializer<Map<String,?>> deserializer);
/**
* Returns {@code true} if the specified JWT compact string represents a signed JWT (aka a 'JWS'), {@code false}
* otherwise.

View File

@ -71,7 +71,7 @@ public class DefaultClaims extends JwtMap implements Claims {
@Override
public Claims setExpiration(Date exp) {
setDate(Claims.EXPIRATION, exp);
setDateAsSeconds(Claims.EXPIRATION, exp);
return this;
}
@ -82,7 +82,7 @@ public class DefaultClaims extends JwtMap implements Claims {
@Override
public Claims setNotBefore(Date nbf) {
setDate(Claims.NOT_BEFORE, nbf);
setDateAsSeconds(Claims.NOT_BEFORE, nbf);
return this;
}
@ -93,7 +93,7 @@ public class DefaultClaims extends JwtMap implements Claims {
@Override
public Claims setIssuedAt(Date iat) {
setDate(Claims.ISSUED_AT, iat);
setDateAsSeconds(Claims.ISSUED_AT, iat);
return this;
}
@ -108,25 +108,35 @@ public class DefaultClaims extends JwtMap implements Claims {
return this;
}
/**
* @since 0.10.0
*/
private static boolean isSpecDate(String claimName) {
return Claims.EXPIRATION.equals(claimName) ||
Claims.ISSUED_AT.equals(claimName) ||
Claims.NOT_BEFORE.equals(claimName);
}
@Override
public <T> T get(String claimName, Class<T> requiredType) {
Object value = get(claimName);
if (value == null) { return null; }
if (Claims.EXPIRATION.equals(claimName) ||
Claims.ISSUED_AT.equals(claimName) ||
Claims.NOT_BEFORE.equals(claimName)
) {
value = getDate(claimName);
Object value = get(claimName);
if (value == null) {
return null;
}
if (Date.class.equals(requiredType)) {
if (isSpecDate(claimName)) {
value = toSpecDate(value, claimName);
} else {
value = toDate(value, 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;

View File

@ -15,8 +15,6 @@
*/
package io.jsonwebtoken.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.CompressionCodec;
import io.jsonwebtoken.Header;
@ -29,7 +27,11 @@ import io.jsonwebtoken.codec.Decoder;
import io.jsonwebtoken.codec.Encoder;
import io.jsonwebtoken.impl.crypto.DefaultJwtSigner;
import io.jsonwebtoken.impl.crypto.JwtSigner;
import io.jsonwebtoken.io.SerializationException;
import io.jsonwebtoken.io.Serializer;
import io.jsonwebtoken.io.impl.InstanceLocator;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Classes;
import io.jsonwebtoken.lang.Collections;
import io.jsonwebtoken.lang.Strings;
@ -40,8 +42,6 @@ import java.util.Map;
public class DefaultJwtBuilder implements JwtBuilder {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private Header header;
private Claims claims;
private String payload;
@ -49,10 +49,19 @@ public class DefaultJwtBuilder implements JwtBuilder {
private SignatureAlgorithm algorithm;
private Key key;
private Serializer<Map<String,?>> serializer;
private Encoder<byte[], String> base64UrlEncoder = Encoder.BASE64URL;
private CompressionCodec compressionCodec;
@Override
public JwtBuilder serializeToJsonWith(Serializer<Map<String,?>> serializer) {
Assert.notNull(serializer, "Serializer cannot be null.");
this.serializer = serializer;
return this;
}
@Override
public JwtBuilder base64UrlEncodeWith(Encoder<byte[], String> base64UrlEncoder) {
Assert.notNull(base64UrlEncoder, "base64UrlEncoder cannot be null.");
@ -270,6 +279,14 @@ public class DefaultJwtBuilder implements JwtBuilder {
@Override
public String compact() {
if (this.serializer == null) {
//try to find one based on the runtime environment:
InstanceLocator<Serializer<Map<String,?>>> locator =
Classes.newInstance("io.jsonwebtoken.io.impl.RuntimeClasspathSerializerLocator");
this.serializer = locator.getInstance();
}
if (payload == null && Collections.isEmpty(claims)) {
throw new IllegalStateException("Either 'payload' or 'claims' must be specified.");
}
@ -304,8 +321,8 @@ public class DefaultJwtBuilder implements JwtBuilder {
byte[] bytes;
try {
bytes = this.payload != null ? payload.getBytes(Strings.UTF_8) : toJson(claims);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Unable to serialize claims object to json.");
} catch (SerializationException e) {
throw new IllegalArgumentException("Unable to serialize claims object to json: " + e.getMessage(), e);
}
if (compressionCodec != null) {
@ -339,18 +356,25 @@ public class DefaultJwtBuilder implements JwtBuilder {
return new DefaultJwtSigner(alg, key, base64UrlEncoder);
}
@Deprecated // remove before 1.0 - call the serializer and base64UrlEncoder directly
protected String base64UrlEncode(Object o, String errMsg) {
Assert.isInstanceOf(Map.class, o, "object argument must be a map.");
Map m = (Map)o;
byte[] bytes;
try {
bytes = toJson(o);
} catch (JsonProcessingException e) {
bytes = toJson(m);
} catch (SerializationException e) {
throw new IllegalStateException(errMsg, e);
}
return base64UrlEncoder.encode(bytes);
}
protected byte[] toJson(Object object) throws JsonProcessingException {
return OBJECT_MAPPER.writeValueAsBytes(object);
@SuppressWarnings("unchecked")
@Deprecated //remove before 1.0 - call the serializer directly
protected byte[] toJson(Object object) throws SerializationException {
Assert.isInstanceOf(Map.class, object, "object argument must be a map.");
Map m = (Map)object;
return serializer.serialize(m);
}
}

View File

@ -15,7 +15,6 @@
*/
package io.jsonwebtoken.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.ClaimJwtException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Clock;
@ -42,26 +41,25 @@ import io.jsonwebtoken.codec.Decoder;
import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver;
import io.jsonwebtoken.impl.crypto.DefaultJwtSignatureValidator;
import io.jsonwebtoken.impl.crypto.JwtSignatureValidator;
import io.jsonwebtoken.io.DeserializationException;
import io.jsonwebtoken.io.Deserializer;
import io.jsonwebtoken.io.impl.InstanceLocator;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Classes;
import io.jsonwebtoken.lang.DateFormats;
import io.jsonwebtoken.lang.Objects;
import io.jsonwebtoken.lang.Strings;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.security.Key;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
@SuppressWarnings("unchecked")
public class DefaultJwtParser implements JwtParser {
//don't need millis since JWT date fields are only second granularity:
private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
private static final int MILLISECONDS_PER_SECOND = 1000;
private ObjectMapper objectMapper = new ObjectMapper();
private byte[] keyBytes;
private Key key;
@ -72,12 +70,21 @@ public class DefaultJwtParser implements JwtParser {
private Decoder<String, byte[]> base64UrlDecoder = Decoder.BASE64URL;
private Deserializer<Map<String, ?>> deserializer;
private Claims expectedClaims = new DefaultClaims();
private Clock clock = DefaultClock.INSTANCE;
private long allowedClockSkewMillis = 0;
@Override
public JwtParser deserializeJsonWith(Deserializer<Map<String, ?>> deserializer) {
Assert.notNull(deserializer, "deserializer cannot be null.");
this.deserializer = deserializer;
return this;
}
@Override
public JwtParser base64UrlDecodeWith(Decoder<String, byte[]> base64UrlDecoder) {
Assert.notNull(base64UrlDecoder, "base64UrlDecoder cannot be null.");
@ -210,6 +217,13 @@ public class DefaultJwtParser implements JwtParser {
@Override
public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, SignatureException {
if (this.deserializer == null) {
//try to find one based on the runtime environment:
InstanceLocator<Deserializer<Map<String, ?>>> locator =
Classes.newInstance("io.jsonwebtoken.io.impl.RuntimeClasspathDeserializerLocator");
this.deserializer = locator.getInstance();
}
Assert.hasText(jwt, "JWT String argument cannot be null or empty.");
String base64UrlEncodedHeader = null;
@ -260,7 +274,7 @@ public class DefaultJwtParser implements JwtParser {
if (base64UrlEncodedHeader != null) {
byte[] bytes = base64UrlDecoder.decode(base64UrlEncodedHeader);
String origValue = new String(bytes, Strings.UTF_8);
Map<String, Object> m = readValue(origValue);
Map<String, Object> m = (Map<String, Object>) readValue(origValue);
if (base64UrlEncodedDigest != null) {
header = new DefaultJwsHeader(m);
@ -281,7 +295,7 @@ public class DefaultJwtParser implements JwtParser {
Claims claims = null;
if (payload.charAt(0) == '{' && payload.charAt(payload.length() - 1) == '}') { //likely to be json, parse it:
Map<String, Object> claimsMap = readValue(payload);
Map<String, Object> claimsMap = (Map<String, Object>) readValue(payload);
claims = new DefaultClaims(claimsMap);
}
@ -369,8 +383,6 @@ public class DefaultJwtParser implements JwtParser {
//since 0.3:
if (claims != null) {
SimpleDateFormat sdf;
final Date now = this.clock.now();
long nowTime = now.getTime();
@ -382,9 +394,8 @@ public class DefaultJwtParser implements JwtParser {
long maxTime = nowTime - this.allowedClockSkewMillis;
Date max = allowSkew ? new Date(maxTime) : now;
if (max.after(exp)) {
sdf = new SimpleDateFormat(ISO_8601_FORMAT);
String expVal = sdf.format(exp);
String nowVal = sdf.format(now);
String expVal = DateFormats.formatIso8601(exp, false);
String nowVal = DateFormats.formatIso8601(now, false);
long differenceMillis = maxTime - exp.getTime();
@ -403,9 +414,8 @@ public class DefaultJwtParser implements JwtParser {
long minTime = nowTime + this.allowedClockSkewMillis;
Date min = allowSkew ? new Date(minTime) : now;
if (min.before(nbf)) {
sdf = new SimpleDateFormat(ISO_8601_FORMAT);
String nbfVal = sdf.format(nbf);
String nowVal = sdf.format(now);
String nbfVal = DateFormats.formatIso8601(nbf, false);
String nowVal = DateFormats.formatIso8601(now, false);
long differenceMillis = nbf.getTime() - minTime;
@ -423,27 +433,37 @@ public class DefaultJwtParser implements JwtParser {
Object body = claims != null ? claims : payload;
if (base64UrlEncodedDigest != null) {
return new DefaultJws<Object>((JwsHeader) header, body, base64UrlEncodedDigest);
return new DefaultJws<>((JwsHeader) header, body, base64UrlEncodedDigest);
} else {
return new DefaultJwt<Object>(header, body);
return new DefaultJwt<>(header, body);
}
}
/**
* @since 0.10.0
*/
private static Object normalize(Object o) {
if (o instanceof Integer) {
o = ((Integer)o).longValue();
}
return o;
}
private void validateExpectedClaims(Header header, Claims claims) {
for (String expectedClaimName : expectedClaims.keySet()) {
Object expectedClaimValue = expectedClaims.get(expectedClaimName);
Object actualClaimValue = claims.get(expectedClaimName);
Object expectedClaimValue = normalize(expectedClaims.get(expectedClaimName));
Object actualClaimValue = normalize(claims.get(expectedClaimName));
if (Claims.ISSUED_AT.equals(expectedClaimName) || Claims.EXPIRATION.equals(expectedClaimName) ||
Claims.NOT_BEFORE.equals(expectedClaimName)) {
expectedClaimValue = expectedClaims.get(expectedClaimName, Date.class);
if (expectedClaimValue instanceof Date) {
try {
actualClaimValue = claims.get(expectedClaimName, Date.class);
} else if (expectedClaimValue instanceof Date && actualClaimValue instanceof Long) {
actualClaimValue = new Date((Long) actualClaimValue);
} catch (Exception e) {
String msg = "JWT Claim '" + expectedClaimName + "' was expected to be a Date, but its value " +
"cannot be converted to a Date using current heuristics. Value: " + actualClaimValue;
throw new IncorrectClaimException(header, claims, msg);
}
}
InvalidClaimException invalidClaimException = null;
@ -553,10 +573,11 @@ public class DefaultJwtParser implements JwtParser {
}
@SuppressWarnings("unchecked")
protected Map<String, Object> readValue(String val) {
protected Map<String, ?> readValue(String val) {
try {
return objectMapper.readValue(val, Map.class);
} catch (IOException e) {
byte[] bytes = val.getBytes(Strings.UTF_8);
return deserializer.deserialize(bytes);
} catch (DeserializationException e) {
throw new MalformedJwtException("Unable to read JSON value: " + val, e);
}
}

View File

@ -16,14 +16,17 @@
package io.jsonwebtoken.impl;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.DateFormats;
import java.text.ParseException;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
public class JwtMap implements Map<String,Object> {
public class JwtMap implements Map<String, Object> {
private final Map<String, Object> map;
@ -46,24 +49,56 @@ public class JwtMap implements Map<String,Object> {
return null;
} else if (v instanceof Date) {
return (Date) v;
} else if (v instanceof Calendar) { //since 0.10.0
return ((Calendar) v).getTime();
} else if (v instanceof Number) {
//assume millis:
long millis = ((Number) v).longValue();
return new Date(millis);
} else if (v instanceof String) {
return parseIso8601Date((String) v, name); //ISO-8601 parsing since 0.10.0
} else {
throw new IllegalStateException("Cannot create Date from '" + name + "' value '" + v + "'.");
}
}
/**
* @since 0.10.0
*/
private static Date parseIso8601Date(String s, String name) throws IllegalArgumentException {
try {
return DateFormats.parseIso8601Date(s);
} catch (ParseException e) {
String msg = "'" + name + "' value does not appear to be ISO-8601-formatted: " + s;
throw new IllegalArgumentException(msg, e);
}
}
/**
* @since 0.10.0
*/
protected static Date toSpecDate(Object v, String name) {
if (v == null) {
return null;
} else if (v instanceof Number) {
// https://github.com/jwtk/jjwt/issues/122:
// The JWT RFC *mandates* NumericDate values are represented as seconds.
// Because Because java.util.Date requires milliseconds, we need to multiply by 1000:
long seconds = ((Number) v).longValue();
long millis = seconds * 1000;
return new Date(millis);
v = seconds * 1000;
} else if (v instanceof String) {
// https://github.com/jwtk/jjwt/issues/122
// The JWT RFC *mandates* NumericDate values are represented as seconds.
// Because Because java.util.Date requires milliseconds, we need to multiply by 1000:
try {
long seconds = Long.parseLong((String) v);
long millis = seconds * 1000;
return new Date(millis);
} else {
throw new IllegalStateException("Cannot convert '" + name + "' value [" + v + "] to Date instance.");
v = seconds * 1000;
} catch (NumberFormatException ignored) {
}
}
//v would have been normalized to milliseconds if it was a number value, so perform normal date conversion:
return toDate(v, name);
}
protected void setValue(String name, Object v) {
if (v == null) {
@ -73,12 +108,17 @@ public class JwtMap implements Map<String,Object> {
}
}
protected Date getDate(String name) {
Object v = map.get(name);
return toDate(v, name);
@Deprecated //remove just before 1.0.0
protected void setDate(String name, Date d) {
if (d == null) {
map.remove(name);
} else {
long seconds = d.getTime() / 1000;
map.put(name, seconds);
}
}
protected void setDate(String name, Date d) {
protected void setDateAsSeconds(String name, Date d) {
if (d == null) {
map.remove(name);
} else {

View File

@ -0,0 +1,12 @@
package io.jsonwebtoken.io;
public class DeserializationException extends SerialException {
public DeserializationException(String msg) {
super(msg);
}
public DeserializationException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,6 @@
package io.jsonwebtoken.io;
public interface Deserializer<T> {
T deserialize(byte[] bytes) throws DeserializationException;
}

View File

@ -0,0 +1,14 @@
package io.jsonwebtoken.io;
import io.jsonwebtoken.JwtException;
public class IOException extends JwtException {
public IOException(String msg) {
super(msg);
}
public IOException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,12 @@
package io.jsonwebtoken.io;
public class SerialException extends IOException {
public SerialException(String msg) {
super(msg);
}
public SerialException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,12 @@
package io.jsonwebtoken.io;
public class SerializationException extends SerialException {
public SerializationException(String msg) {
super(msg);
}
public SerializationException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,7 @@
package io.jsonwebtoken.io;
public interface Serializer<T> {
byte[] serialize(T t) throws SerializationException;
}

View File

@ -0,0 +1,6 @@
package io.jsonwebtoken.io.impl;
public interface InstanceLocator<T> {
T getInstance();
}

View File

@ -0,0 +1,48 @@
package io.jsonwebtoken.io.impl;
import io.jsonwebtoken.io.Deserializer;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Classes;
import java.util.concurrent.atomic.AtomicReference;
public class RuntimeClasspathDeserializerLocator<T> implements InstanceLocator<Deserializer<T>> {
private static final AtomicReference<Deserializer> DESERIALIZER = new AtomicReference<>();
@SuppressWarnings("unchecked")
@Override
public Deserializer<T> getInstance() {
Deserializer<T> deserializer = DESERIALIZER.get();
if (deserializer == null) {
deserializer = locate();
Assert.state(deserializer != null, "locate() cannot return null.");
if (!compareAndSet(deserializer)) {
deserializer = DESERIALIZER.get();
}
}
Assert.state(deserializer != null, "deserializer cannot be null.");
return deserializer;
}
@SuppressWarnings("WeakerAccess") //to allow testing override
protected Deserializer<T> locate() {
if (isAvailable("com.fasterxml.jackson.databind.ObjectMapper")) {
return Classes.newInstance("io.jsonwebtoken.io.impl.jackson.JacksonDeserializer");
} else if (isAvailable("org.json.JSONObject")) {
return Classes.newInstance("io.jsonwebtoken.io.impl.orgjson.OrgJsonDeserializer");
} else {
throw new IllegalStateException("Unable to discover any JSON Deserializer implementations on the classpath.");
}
}
@SuppressWarnings("WeakerAccess") //to allow testing override
protected boolean compareAndSet(Deserializer<T> d) {
return DESERIALIZER.compareAndSet(null, d);
}
@SuppressWarnings("WeakerAccess") //to allow testing override
protected boolean isAvailable(String fqcn) {
return Classes.isAvailable(fqcn);
}
}

View File

@ -0,0 +1,48 @@
package io.jsonwebtoken.io.impl;
import io.jsonwebtoken.io.Serializer;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Classes;
import java.util.concurrent.atomic.AtomicReference;
public class RuntimeClasspathSerializerLocator implements InstanceLocator<Serializer> {
private static final AtomicReference<Serializer<Object>> SERIALIZER = new AtomicReference<>();
@SuppressWarnings("unchecked")
@Override
public Serializer<Object> getInstance() {
Serializer<Object> serializer = SERIALIZER.get();
if (serializer == null) {
serializer = locate();
Assert.state(serializer != null, "locate() cannot return null.");
if (!compareAndSet(serializer)) {
serializer = SERIALIZER.get();
}
}
Assert.state(serializer != null, "serializer cannot be null.");
return serializer;
}
@SuppressWarnings("WeakerAccess") //to allow testing override
protected Serializer<Object> locate() {
if (isAvailable("com.fasterxml.jackson.databind.ObjectMapper")) {
return Classes.newInstance("io.jsonwebtoken.io.impl.jackson.JacksonSerializer");
} else if (isAvailable("org.json.JSONObject")) {
return Classes.newInstance("io.jsonwebtoken.io.impl.orgjson.OrgJsonSerializer");
} else {
throw new IllegalStateException("Unable to discover any JSON Serializer implementations on the classpath.");
}
}
@SuppressWarnings("WeakerAccess") //to allow testing override
protected boolean compareAndSet(Serializer<Object> s) {
return SERIALIZER.compareAndSet(null, s);
}
@SuppressWarnings("WeakerAccess") //to allow testing override
protected boolean isAvailable(String fqcn) {
return Classes.isAvailable(fqcn);
}
}

View File

@ -0,0 +1,47 @@
package io.jsonwebtoken.io.impl.jackson;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.io.DeserializationException;
import io.jsonwebtoken.io.Deserializer;
import io.jsonwebtoken.lang.Assert;
import java.io.IOException;
import java.util.Map;
@SuppressWarnings("unused") //used via reflection by RuntimeClasspathDeserializerLocator
public class JacksonDeserializer<T> implements Deserializer<T> {
private final Class<T> returnType;
private final ObjectMapper objectMapper;
@SuppressWarnings("unused") //used via reflection by RuntimeClasspathDeserializerLocator
public JacksonDeserializer() {
this(JacksonSerializer.DEFAULT_OBJECT_MAPPER);
}
@SuppressWarnings({"unchecked", "WeakerAccess", "unused"}) // for end-users providing a custom ObjectMapper
public JacksonDeserializer(ObjectMapper objectMapper) {
this(objectMapper, (Class<T>) Map.class);
}
private JacksonDeserializer(ObjectMapper objectMapper, Class<T> returnType) {
Assert.notNull(objectMapper, "ObjectMapper cannot be null.");
Assert.notNull(returnType, "Return type cannot be null.");
this.objectMapper = objectMapper;
this.returnType = returnType;
}
@Override
public T deserialize(byte[] bytes) throws DeserializationException {
try {
return readValue(bytes);
} catch (IOException e) {
String msg = "Unable to deserialize bytes into a " + returnType.getName() + " instance: " + e.getMessage();
throw new DeserializationException(msg, e);
}
}
protected T readValue(byte[] bytes) throws IOException {
return objectMapper.readValue(bytes, returnType);
}
}

View File

@ -0,0 +1,41 @@
package io.jsonwebtoken.io.impl.jackson;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.io.SerializationException;
import io.jsonwebtoken.io.Serializer;
import io.jsonwebtoken.lang.Assert;
public class JacksonSerializer<T> implements Serializer<T> {
static final ObjectMapper DEFAULT_OBJECT_MAPPER = new ObjectMapper();
private final ObjectMapper objectMapper;
@SuppressWarnings("unused") //used via reflection by RuntimeClasspathDeserializerLocator
public JacksonSerializer() {
this(DEFAULT_OBJECT_MAPPER);
}
@SuppressWarnings("WeakerAccess") //intended for end-users to use when providing a custom ObjectMapper
public JacksonSerializer(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "ObjectMapper cannot be null.");
this.objectMapper = objectMapper;
}
@Override
public byte[] serialize(T t) throws SerializationException {
Assert.notNull(t, "Object to serialize cannot be null.");
try {
return writeValueAsBytes(t);
} catch (JsonProcessingException e) {
String msg = "Unable to serialize object: " + e.getMessage();
throw new SerializationException(msg, e);
}
}
@SuppressWarnings("WeakerAccess") //for testing
protected byte[] writeValueAsBytes(T t) throws JsonProcessingException {
return this.objectMapper.writeValueAsBytes(t);
}
}

View File

@ -0,0 +1,88 @@
package io.jsonwebtoken.io.impl.orgjson;
import io.jsonwebtoken.io.DeserializationException;
import io.jsonwebtoken.io.Deserializer;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Strings;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class OrgJsonDeserializer implements Deserializer<Object> {
@SuppressWarnings("unchecked")
@Override
public Object deserialize(byte[] bytes) throws DeserializationException {
Assert.notNull(bytes, "JSON byte array cannot be null");
if (bytes.length == 0) {
throw new DeserializationException("Invalid JSON: zero length byte array.");
}
try {
String s = new String(bytes, Strings.UTF_8);
return parse(s);
} catch (Exception e) {
String msg = "Invalid JSON: " + e.getMessage();
throw new DeserializationException(msg, e);
}
}
private Object parse(String json) throws JSONException {
JSONTokener tokener = new JSONTokener(json);
char c = tokener.nextClean(); //peak ahead
tokener.back(); //revert
if (c == '{') { //json object
JSONObject o = new JSONObject(tokener);
return toMap(o);
} else if ( c == '[' ) {
JSONArray a = new JSONArray(tokener);
return toList(a);
} else {
//raw json value
Object value = tokener.nextValue();
return convertIfNecessary(value);
}
}
private Map<String, Object> toMap(JSONObject o) {
Map<String, Object> map = new LinkedHashMap<>();
for (String key : o.keySet()) {
Object value = o.get(key);
value = convertIfNecessary(value);
map.put(key, value);
}
return map;
}
private List<Object> toList(JSONArray a) {
List<Object> list = new ArrayList<>(a.length());
for (Object value : a.toList()) {
value = convertIfNecessary(value);
list.add(value);
}
return list;
}
private Object convertIfNecessary(Object v) {
Object value = v;
if (JSONObject.NULL.equals(value)) {
value = null;
} else if (value instanceof JSONArray) {
value = toList((JSONArray) value);
} else if (value instanceof JSONObject) {
value = toMap((JSONObject) value);
}
return value;
}
}

View File

@ -0,0 +1,124 @@
package io.jsonwebtoken.io.impl.orgjson;
import io.jsonwebtoken.codec.Encoder;
import io.jsonwebtoken.io.SerializationException;
import io.jsonwebtoken.io.Serializer;
import io.jsonwebtoken.lang.Collections;
import io.jsonwebtoken.lang.DateFormats;
import io.jsonwebtoken.lang.Objects;
import io.jsonwebtoken.lang.Strings;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONString;
import org.json.JSONWriter;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.Map;
public class OrgJsonSerializer<T> implements Serializer<T> {
@Override
public byte[] serialize(T t) throws SerializationException {
try {
Object o = toJSONInstance(t);
return toBytes(o);
} catch (SerializationException se) {
//propagate
throw se;
} catch (Exception e) {
String msg = "Unable to serialize object of type " + t.getClass().getName() + " to JSON: " + e.getMessage();
throw new SerializationException(msg, e);
}
}
private Object toJSONInstance(Object object) {
if (object == null) {
return JSONObject.NULL;
}
if (object instanceof JSONObject || object instanceof JSONArray
|| JSONObject.NULL.equals(object) || object instanceof JSONString
|| object instanceof Byte || object instanceof Character
|| object instanceof Short || object instanceof Integer
|| object instanceof Long || object instanceof Boolean
|| object instanceof Float || object instanceof Double
|| object instanceof String || object instanceof BigInteger
|| object instanceof BigDecimal || object instanceof Enum) {
return object;
}
if (object instanceof Calendar) {
object = ((Calendar)object).getTime(); //sets object to date, will be converted in next if-statement:
}
if (object instanceof Date) {
Date date = (Date)object;
return DateFormats.formatIso8601(date);
}
if (object instanceof byte[]) {
return Encoder.BASE64.encode((byte[])object);
}
if (object instanceof char[]) {
return new String((char[])object);
}
if (object instanceof Map) {
Map<?, ?> map = (Map<?, ?>) object;
return toJSONObject(map);
}
if (object instanceof Collection) {
Collection<?> coll = (Collection<?>) object;
return toJSONArray(coll);
}
if (Objects.isArray(object)) {
Collection c = Collections.arrayToList(object);
return toJSONArray(c);
}
//not an immediately JSON-compatible object and probably a JavaBean (or similar). We can't convert that
//directly without using a marshaller of some sort:
String msg = "Unable to serialize object of type " + object.getClass().getName() + " to JSON using known heuristics.";
throw new SerializationException(msg);
}
private JSONObject toJSONObject(Map<?,?> m) {
JSONObject obj = new JSONObject();
for(Map.Entry<?,?> entry : m.entrySet()) {
Object k = entry.getKey();
Object value = entry.getValue();
String key = String.valueOf(k);
value = toJSONInstance(value);
obj.put(key, value);
}
return obj;
}
private JSONArray toJSONArray(Collection c) {
JSONArray array = new JSONArray();
for(Object o : c) {
o = toJSONInstance(o);
array.put(o);
}
return array;
}
@SuppressWarnings("WeakerAccess") //for testing
protected byte[] toBytes(Object o) {
String s = JSONWriter.valueToString(o);
return s.getBytes(Strings.UTF_8);
}
}

View File

@ -69,7 +69,8 @@ public final class Classes {
* @return the located class
* @throws UnknownClassException if the class cannot be found.
*/
public static Class forName(String fqcn) throws UnknownClassException {
@SuppressWarnings("unchecked")
public static <T> Class<T> forName(String fqcn) throws UnknownClassException {
Class clazz = THREAD_CL_ACCESSOR.loadClass(fqcn);
@ -132,13 +133,13 @@ public final class Classes {
}
@SuppressWarnings("unchecked")
public static Object newInstance(String fqcn) {
return newInstance(forName(fqcn));
public static <T> T newInstance(String fqcn) {
return (T)newInstance(forName(fqcn));
}
@SuppressWarnings("unchecked")
public static Object newInstance(String fqcn, Object... args) {
return newInstance(forName(fqcn), args);
public static <T> T newInstance(String fqcn, Object... args) {
return (T)newInstance(forName(fqcn), args);
}
public static <T> T newInstance(Class<T> clazz) {

View File

@ -0,0 +1,50 @@
package io.jsonwebtoken.lang;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @since 0.10.0
*/
public class DateFormats {
private static final String ISO_8601_PATTERN = "yyyy-MM-dd'T'HH:mm:ss'Z'";
private static final String ISO_8601_MILLIS_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
private static final ThreadLocal<DateFormat> ISO_8601 = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat(ISO_8601_PATTERN);
}
};
private static final ThreadLocal<DateFormat> ISO_8601_MILLIS = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat(ISO_8601_MILLIS_PATTERN);
}
};
public static String formatIso8601(Date date) {
return formatIso8601(date, true);
}
public static String formatIso8601(Date date, boolean includeMillis) {
if (includeMillis) {
return ISO_8601_MILLIS.get().format(date);
}
return ISO_8601.get().format(date);
}
public static Date parseIso8601Date(String s) throws ParseException {
Assert.notNull(s, "String argument cannot be null.");
if (s.lastIndexOf('.') > -1) { //assume ISO-8601 with milliseconds
return ISO_8601_MILLIS.get().parse(s);
} else { //assume ISO-8601 without millis:
return ISO_8601.get().parse(s);
}
}
}

View File

@ -24,13 +24,13 @@ import org.junit.Test
import javax.crypto.spec.SecretKeySpec
import java.security.SecureRandom
import static org.junit.Assert.*
import static ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE
import static ClaimJwtException.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE
import static org.junit.Assert.*
class JwtParserTest {
private static final SecureRandom random = new SecureRandom(); //doesn't need to be seeded - just testing
private static final SecureRandom random = new SecureRandom() //doesn't need to be seeded - just testing
protected static byte[] randomKey() {
//create random signing key for testing:
@ -826,15 +826,11 @@ class JwtParserTest {
byte[] key = randomKey()
// not setting expected claim name in JWT
String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key).
setIssuer('Dummy').
compact()
String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key).setIssuer('Dummy').compact()
try {
// expecting null claim name, but with value
Jwt<Header, Claims> jwt = Jwts.parser().setSigningKey(key).
require(null, expectedClaimValue).
parseClaimsJws(compact)
Jwts.parser().setSigningKey(key).require(null, expectedClaimValue).parseClaimsJws(compact)
fail()
} catch (IllegalArgumentException e) {
assertEquals(
@ -876,9 +872,7 @@ class JwtParserTest {
byte[] key = randomKey()
// not setting expected claim name in JWT
String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key).
setIssuer('Dummy').
compact()
String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key).setIssuer('Dummy').compact()
try {
// expecting claim name, but with null value
@ -964,6 +958,7 @@ class JwtParserTest {
@Test
void testParseRequireIssuedAt_Success() {
def issuedAt = new Date(System.currentTimeMillis())
byte[] key = randomKey()
@ -982,7 +977,7 @@ class JwtParserTest {
assertEquals jwt.getBody().getIssuedAt().getTime(), issuedAtMillis, 0
}
@Test
@Test(expected = IncorrectClaimException)
void testParseRequireIssuedAt_Incorrect_Fail() {
def goodIssuedAt = new Date(System.currentTimeMillis())
def badIssuedAt = new Date(System.currentTimeMillis() - 10000)
@ -993,20 +988,12 @@ class JwtParserTest {
setIssuedAt(badIssuedAt).
compact()
try {
Jwts.parser().setSigningKey(key).
requireIssuedAt(goodIssuedAt).
parseClaimsJws(compact)
fail()
} catch(IncorrectClaimException e) {
assertEquals(
String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.ISSUED_AT, goodIssuedAt, badIssuedAt),
e.getMessage()
)
}
}
@Test
@Test(expected = MissingClaimException)
void testParseRequireIssuedAt_Missing_Fail() {
def issuedAt = new Date(System.currentTimeMillis() - 10000)
@ -1016,17 +1003,9 @@ class JwtParserTest {
setSubject("Dummy").
compact()
try {
Jwts.parser().setSigningKey(key).
requireIssuedAt(issuedAt).
parseClaimsJws(compact)
fail()
} catch(MissingClaimException e) {
assertEquals(
String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.ISSUED_AT, issuedAt),
e.getMessage()
)
}
}
@Test
@ -1306,7 +1285,7 @@ class JwtParserTest {
assertEquals jwt.getBody().getExpiration().getTime(), expirationMillis, 0
}
@Test
@Test(expected = IncorrectClaimException)
void testParseRequireExpirationAt_Incorrect_Fail() {
def goodExpiration = new Date(System.currentTimeMillis() + 20000)
def badExpiration = new Date(System.currentTimeMillis() + 10000)
@ -1317,20 +1296,12 @@ class JwtParserTest {
setExpiration(badExpiration).
compact()
try {
Jwts.parser().setSigningKey(key).
requireExpiration(goodExpiration).
parseClaimsJws(compact)
fail()
} catch(IncorrectClaimException e) {
assertEquals(
String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.EXPIRATION, goodExpiration, badExpiration),
e.getMessage()
)
}
}
@Test
@Test(expected = MissingClaimException)
void testParseRequireExpiration_Missing_Fail() {
def expiration = new Date(System.currentTimeMillis() + 10000)
@ -1340,17 +1311,9 @@ class JwtParserTest {
setSubject("Dummy").
compact()
try {
Jwts.parser().setSigningKey(key).
requireExpiration(expiration).
parseClaimsJws(compact)
fail()
} catch(MissingClaimException e) {
assertEquals(
String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.EXPIRATION, expiration),
e.getMessage()
)
}
}
@Test
@ -1374,7 +1337,7 @@ class JwtParserTest {
assertEquals jwt.getBody().getNotBefore().getTime(), notBeforeMillis, 0
}
@Test
@Test(expected = IncorrectClaimException)
void testParseRequireNotBefore_Incorrect_Fail() {
def goodNotBefore = new Date(System.currentTimeMillis() - 20000)
def badNotBefore = new Date(System.currentTimeMillis() - 10000)
@ -1385,20 +1348,12 @@ class JwtParserTest {
setNotBefore(badNotBefore).
compact()
try {
Jwts.parser().setSigningKey(key).
requireNotBefore(goodNotBefore).
parseClaimsJws(compact)
fail()
} catch(IncorrectClaimException e) {
assertEquals(
String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.NOT_BEFORE, goodNotBefore, badNotBefore),
e.getMessage()
)
}
}
@Test
@Test(expected = MissingClaimException)
void testParseRequireNotBefore_Missing_Fail() {
def notBefore = new Date(System.currentTimeMillis() - 10000)
@ -1408,21 +1363,14 @@ class JwtParserTest {
setSubject("Dummy").
compact()
try {
Jwts.parser().setSigningKey(key).
requireNotBefore(notBefore).
parseClaimsJws(compact)
fail()
} catch(MissingClaimException e) {
assertEquals(
String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.NOT_BEFORE, notBefore),
e.getMessage()
)
}
}
@Test
void testParseRequireCustomDate_Success() {
def aDate = new Date(System.currentTimeMillis())
byte[] key = randomKey()
@ -1438,8 +1386,33 @@ class JwtParserTest {
assertEquals jwt.getBody().get("aDate", Date.class), aDate
}
@Test //since 0.10.0
void testParseRequireCustomDateWhenClaimIsNotADate() {
def goodDate = new Date(System.currentTimeMillis())
def badDate = 'hello'
byte[] key = randomKey()
String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key).
claim("aDate", badDate).
compact()
try {
Jwts.parser().setSigningKey(key).
require("aDate", goodDate).
parseClaimsJws(compact)
fail()
} catch(IncorrectClaimException e) {
String expected = 'JWT Claim \'aDate\' was expected to be a Date, but its value cannot be converted to a ' +
'Date using current heuristics. Value: hello'
assertEquals expected, e.getMessage()
}
}
@Test
void testParseRequireCustomDate_Incorrect_Fail() {
def goodDate = new Date(System.currentTimeMillis())
def badDate = new Date(System.currentTimeMillis() - 10000)
@ -1460,7 +1433,6 @@ class JwtParserTest {
e.getMessage()
)
}
}
@Test
@ -1534,18 +1506,17 @@ class JwtParserTest {
String bad = base64Url(header) + '.' + base64Url(payload) + '.' + base64Url(badSig) + '.' + base64Url(bogus)
try {
Jwts.parser().setSigningKey(randomKey()).parse(bad)
fail()
} catch (MalformedJwtException se) {
assertEquals 'JWT strings must contain exactly 2 period characters. Found: 3', se.message
}
}
@Test
void testNoHeaderNoSig() {
String payload = '{"subject":"Joe"}'
String jwtStr = '.' + base64Url(payload) + '.'
@ -1575,16 +1546,17 @@ class JwtParserTest {
@Test
void testBadHeaderSig() {
String header = '{"alg":"none"}'
String payload = '{"subject":"Joe"}'
String sig = ";aklsjdf;kajsd;fkjas;dklfj"
String jwtStr = base64Url(payload) + '.' + base64Url(payload) + '.' + base64Url(sig)
String jwtStr = base64Url(header) + '.' + base64Url(payload) + '.' + base64Url(sig)
try {
Jwt jwt = Jwts.parser().parse(jwtStr)
Jwts.parser().parse(jwtStr)
fail()
} catch (MalformedJwtException se) {
assertEquals 'JWT string has a digest/signature, but the header does not reference a valid signature algorithm.', se.message

View File

@ -15,7 +15,6 @@
*/
package io.jsonwebtoken
import com.fasterxml.jackson.databind.ObjectMapper
import io.jsonwebtoken.codec.Encoder
import io.jsonwebtoken.impl.DefaultHeader
import io.jsonwebtoken.impl.DefaultJwsHeader
@ -25,6 +24,7 @@ import io.jsonwebtoken.impl.compression.GzipCompressionCodec
import io.jsonwebtoken.impl.crypto.EllipticCurveProvider
import io.jsonwebtoken.impl.crypto.MacProvider
import io.jsonwebtoken.impl.crypto.RsaProvider
import io.jsonwebtoken.io.impl.RuntimeClasspathSerializerLocator
import io.jsonwebtoken.lang.Strings
import org.junit.Test
@ -44,6 +44,12 @@ class JwtsTest {
return Encoder.BASE64URL.encode(bytes)
}
protected static String toJson(o) {
def serializer = new RuntimeClasspathSerializerLocator().getInstance()
byte[] bytes = serializer.serialize(o)
return new String(bytes, Strings.UTF_8)
}
@Test
void testSubclass() {
new Jwts()
@ -617,9 +623,8 @@ class JwtsTest {
PublicKey publicKey = kp.getPublic();
PrivateKey privateKey = kp.getPrivate();
ObjectMapper om = new ObjectMapper()
String header = base64Url(om.writeValueAsString(['alg': 'HS256']))
String body = base64Url(om.writeValueAsString('foo'))
String header = base64Url(toJson(['alg': 'HS256']))
String body = base64Url(toJson('foo'))
String compact = header + '.' + body + '.'
// Now for the forgery: simulate an attacker using the RSA public key to sign a token, but
@ -650,9 +655,8 @@ class JwtsTest {
PublicKey publicKey = kp.getPublic();
//PrivateKey privateKey = kp.getPrivate();
ObjectMapper om = new ObjectMapper()
String header = base64Url(om.writeValueAsString(['alg': 'HS256']))
String body = base64Url(om.writeValueAsString('foo'))
String header = base64Url(toJson(['alg': 'HS256']))
String body = base64Url(toJson('foo'))
String compact = header + '.' + body + '.'
// Now for the forgery: simulate an attacker using the RSA public key to sign a token, but
@ -683,9 +687,8 @@ class JwtsTest {
PublicKey publicKey = kp.getPublic();
//PrivateKey privateKey = kp.getPrivate();
ObjectMapper om = new ObjectMapper()
String header = base64Url(om.writeValueAsString(['alg': 'HS256']))
String body = base64Url(om.writeValueAsString('foo'))
String header = base64Url(toJson(['alg': 'HS256']))
String body = base64Url(toJson('foo'))
String compact = header + '.' + body + '.'
// Now for the forgery: simulate an attacker using the Elliptic Curve public key to sign a token, but

View File

@ -15,14 +15,14 @@
*/
package io.jsonwebtoken.impl
import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.JsonMappingException
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm
import io.jsonwebtoken.codec.Encoder
import io.jsonwebtoken.codec.EncodingException
import io.jsonwebtoken.impl.compression.CompressionCodecs
import io.jsonwebtoken.impl.crypto.MacProvider
import io.jsonwebtoken.io.SerializationException
import io.jsonwebtoken.io.impl.orgjson.OrgJsonSerializer
import org.junit.Test
import static org.junit.Assert.*
@ -181,8 +181,8 @@ class DefaultJwtBuilderTest {
def b = new DefaultJwtBuilder() {
@Override
protected byte[] toJson(Object o) throws JsonProcessingException {
throw new JsonMappingException('foo')
protected byte[] toJson(Object o) throws SerializationException {
throw new SerializationException('foo', new Exception())
}
}
@ -192,16 +192,17 @@ class DefaultJwtBuilderTest {
} catch (IllegalStateException ise) {
assertEquals ise.cause.message, 'foo'
}
}
@Test
void testCompactCompressionCodecJsonProcessingException() {
def b = new DefaultJwtBuilder() {
@Override
protected byte[] toJson(Object o) throws JsonProcessingException {
if (o instanceof DefaultJwsHeader) { return super.toJson(o) }
throw new JsonProcessingException('simulate json processing exception on claims')
protected byte[] toJson(Object o) throws SerializationException {
if (o instanceof DefaultJwsHeader) {
return super.toJson(o)
}
throw new SerializationException('dummy text', new Exception())
}
}
@ -211,7 +212,7 @@ class DefaultJwtBuilderTest {
b.setClaims(c).compressWith(CompressionCodecs.DEFLATE).compact()
fail()
} catch (IllegalArgumentException iae) {
assertEquals iae.message, 'Unable to serialize claims object to json.'
assertEquals iae.message, 'Unable to serialize claims object to json: dummy text'
}
}
@ -324,4 +325,24 @@ class DefaultJwtBuilderTest {
assertSame encoder, b.base64UrlEncoder
}
@Test(expected = IllegalArgumentException)
void testSerializeToJsonWithNullArgument() {
new DefaultJwtBuilder().serializeToJsonWith(null)
}
@Test
void testSerializeToJsonWithCustomSerializer() {
def serializer = new OrgJsonSerializer()
def b = new DefaultJwtBuilder().serializeToJsonWith(serializer)
assertSame serializer, b.serializer
def key = MacProvider.generateKey(SignatureAlgorithm.HS256)
String jws = b.signWith(SignatureAlgorithm.HS256, key)
.claim('foo', 'bar')
.compact()
assertEquals 'bar', Jwts.parser().setSigningKey(key).parseClaimsJws(jws).getBody().get('foo')
}
}

View File

@ -1,9 +1,21 @@
package io.jsonwebtoken.impl
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.MalformedJwtException
import io.jsonwebtoken.SignatureAlgorithm
import io.jsonwebtoken.codec.Decoder
import io.jsonwebtoken.codec.DecodingException
import io.jsonwebtoken.codec.Encoder
import io.jsonwebtoken.impl.crypto.MacProvider
import io.jsonwebtoken.io.impl.orgjson.OrgJsonDeserializer
import io.jsonwebtoken.lang.Strings
import org.junit.Test
import static org.junit.Assert.*
import javax.crypto.Mac
import javax.crypto.SecretKey
import static org.junit.Assert.assertEquals
import static org.junit.Assert.assertSame
// NOTE to the casual reader: even though this test class appears mostly empty, the DefaultJwtParser
// implementation is tested to 100% coverage. The vast majority of its tests are in the JwtsTest class. This class
@ -13,11 +25,11 @@ class DefaultJwtParserTest {
@Test(expected = IllegalArgumentException)
void testBase64UrlDecodeWithNullArgument() {
new DefaultJwtBuilder().base64UrlEncodeWith(null)
new DefaultJwtParser().base64UrlDecodeWith(null)
}
@Test
void testBase64UrlEncodeWithCustomEncoder() {
void testBase64UrlEncodeWithCustomDecoder() {
def decoder = new Decoder() {
@Override
Object decode(Object o) throws DecodingException {
@ -27,4 +39,76 @@ class DefaultJwtParserTest {
def b = new DefaultJwtParser().base64UrlDecodeWith(decoder)
assertSame decoder, b.base64UrlDecoder
}
@Test(expected = IllegalArgumentException)
void testDeserializeJsonWithNullArgument() {
new DefaultJwtParser().deserializeJsonWith(null)
}
@Test
void testDesrializeJsonWithCustomSerializer() {
def deserializer = new OrgJsonDeserializer()
def p = new DefaultJwtParser().deserializeJsonWith(deserializer)
assertSame deserializer, p.deserializer
def key = MacProvider.generateKey(SignatureAlgorithm.HS256)
String jws = Jwts.builder().claim('foo', 'bar').signWith(SignatureAlgorithm.HS256, key).compact()
assertEquals 'bar', p.setSigningKey(key).parseClaimsJws(jws).getBody().get('foo')
}
@Test(expected = MalformedJwtException)
void testParseJwsWithMissingAlg() {
String header = Encoder.BASE64URL.encode('{"foo":"bar"}'.getBytes(Strings.UTF_8))
String body = Encoder.BASE64URL.encode('{"hello":"world"}'.getBytes(Strings.UTF_8))
String compact = header + '.' + body + '.'
SecretKey key = MacProvider.generateKey(SignatureAlgorithm.HS256)
Mac mac = Mac.getInstance('HmacSHA256')
mac.init(key)
byte[] signatureBytes = mac.doFinal(compact.getBytes(Strings.UTF_8))
String encodedSignature = Encoder.BASE64URL.encode(signatureBytes)
String invalidJws = compact + encodedSignature
new DefaultJwtParser().setSigningKey(key).parseClaimsJws(invalidJws)
}
@Test(expected = MalformedJwtException)
void testParseJwsWithNullAlg() {
String header = Encoder.BASE64URL.encode('{"alg":null}'.getBytes(Strings.UTF_8))
String body = Encoder.BASE64URL.encode('{"hello":"world"}'.getBytes(Strings.UTF_8))
String compact = header + '.' + body + '.'
SecretKey key = MacProvider.generateKey(SignatureAlgorithm.HS256)
Mac mac = Mac.getInstance('HmacSHA256')
mac.init(key)
byte[] signatureBytes = mac.doFinal(compact.getBytes(Strings.UTF_8))
String encodedSignature = Encoder.BASE64URL.encode(signatureBytes)
String invalidJws = compact + encodedSignature
new DefaultJwtParser().setSigningKey(key).parseClaimsJws(invalidJws)
}
@Test(expected = MalformedJwtException)
void testParseJwsWithEmptyAlg() {
String header = Encoder.BASE64URL.encode('{"alg":" "}'.getBytes(Strings.UTF_8))
String body = Encoder.BASE64URL.encode('{"hello":"world"}'.getBytes(Strings.UTF_8))
String compact = header + '.' + body + '.'
SecretKey key = MacProvider.generateKey(SignatureAlgorithm.HS256)
Mac mac = Mac.getInstance('HmacSHA256')
mac.init(key)
byte[] signatureBytes = mac.doFinal(compact.getBytes(Strings.UTF_8))
String encodedSignature = Encoder.BASE64URL.encode(signatureBytes)
String invalidJws = compact + encodedSignature
new DefaultJwtParser().setSigningKey(key).parseClaimsJws(invalidJws)
}
}

View File

@ -15,7 +15,9 @@
*/
package io.jsonwebtoken.impl
import io.jsonwebtoken.lang.DateFormats
import org.junit.Test
import static org.junit.Assert.*
class JwtMapTest {
@ -28,26 +30,98 @@ class JwtMapTest {
@Test
void testToDateFromDate() {
def d = new Date()
Date date = JwtMap.toDate(d, 'foo')
assertSame date, d
}
@Test
void testToDateFromString() {
Date d = new Date(2015, 1, 1, 12, 0, 0)
String s = (d.getTime() / 1000) + '' //JWT timestamps are in seconds - need to strip millis
Date date = JwtMap.toDate(s, 'foo')
void testToDateFromCalendar() {
def c = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
def d = c.getTime()
Date date = JwtMap.toDate(c, 'foo')
assertEquals date, d
}
@Test
void testToDateFromIso8601String() {
Date d = new Date(2015, 1, 1, 12, 0, 0)
String s = DateFormats.formatIso8601(d, false)
Date date = JwtMap.toDate(s, 'foo')
assertEquals date, d
}
@Test
void testToDateFromInvalidIso8601String() {
Date d = new Date(2015, 1, 1, 12, 0, 0)
String s = d.toString()
try {
JwtMap.toDate(d.toString(), 'foo')
fail()
} catch (IllegalArgumentException iae) {
assertEquals "'foo' value does not appear to be ISO-8601-formatted: $s" as String, iae.getMessage()
}
}
@Test
void testToDateFromIso8601MillisString() {
long millis = System.currentTimeMillis();
Date d = new Date(millis)
String s = DateFormats.formatIso8601(d)
Date date = JwtMap.toDate(s, 'foo')
assertEquals date, d
}
@Test
void testToSpecDateWithNull() {
assertNull JwtMap.toSpecDate(null, 'exp')
}
@Test
void testToSpecDateWithLong() {
long millis = System.currentTimeMillis()
long seconds = (millis / 1000l) as long
Date d = new Date(seconds * 1000)
assertEquals d, JwtMap.toSpecDate(seconds, 'exp')
}
@Test
void testToSpecDateWithString() {
Date d = new Date(2015, 1, 1, 12, 0, 0)
String s = (d.getTime() / 1000) + '' //JWT timestamps are in seconds - need to strip millis
Date date = JwtMap.toSpecDate(s, 'exp')
assertEquals date, d
}
@Test
void testToSpecDateWithIso8601String() {
long millis = System.currentTimeMillis();
Date d = new Date(millis)
String s = DateFormats.formatIso8601(d)
Date date = JwtMap.toSpecDate(s, 'exp')
assertEquals date, d
}
@Test
void testToSpecDateWithDate() {
long millis = System.currentTimeMillis();
Date d = new Date(millis)
Date date = JwtMap.toSpecDate(d, 'exp')
assertSame d, date
}
@Deprecated //remove just before 1.0.0
@Test
void testSetDate() {
def m = new JwtMap()
m.put('foo', 'bar')
m.setDate('foo', null)
assertNull m.get('foo')
long millis = System.currentTimeMillis()
long seconds = (millis / 1000l) as long
Date date = new Date(millis)
m.setDate('foo', date)
assertEquals seconds, m.get('foo')
}
@Test
@ -56,7 +130,7 @@ class JwtMapTest {
JwtMap.toDate(new Object() { @Override public String toString() {return 'hi'} }, 'foo')
fail()
} catch (IllegalStateException iae) {
assertEquals iae.message, "Cannot convert 'foo' value [hi] to Date instance."
assertEquals iae.message, "Cannot create Date from 'foo' value 'hi'."
}
}
@ -126,7 +200,7 @@ class JwtMapTest {
}
@Test
public void testEquals() throws Exception {
void testEquals() throws Exception {
def m1 = new JwtMap();
m1.put("a", "a");
@ -137,7 +211,7 @@ class JwtMapTest {
}
@Test
public void testHashcode() throws Exception {
void testHashcode() throws Exception {
def m = new JwtMap();
def hashCodeEmpty = m.hashCode();

View File

@ -0,0 +1,99 @@
package io.jsonwebtoken.io.impl
import com.fasterxml.jackson.databind.ObjectMapper
import io.jsonwebtoken.io.Deserializer
import io.jsonwebtoken.io.impl.jackson.JacksonDeserializer
import io.jsonwebtoken.io.impl.orgjson.OrgJsonDeserializer
import org.junit.After
import org.junit.Before
import org.junit.Test
import static org.easymock.EasyMock.createMock
import static org.junit.Assert.*
class RuntimeClasspathDeserializerLocatorTest {
@Before
void setUp() {
RuntimeClasspathDeserializerLocator.DESERIALIZER.set(null)
}
@After
void teardown() {
RuntimeClasspathDeserializerLocator.DESERIALIZER.set(null)
}
@Test
void testClassIsNotAvailable() {
def locator = new RuntimeClasspathDeserializerLocator() {
@Override
protected boolean isAvailable(String fqcn) {
return false
}
}
try {
locator.getInstance()
} catch (Exception ex) {
assertEquals 'Unable to discover any JSON Deserializer implementations on the classpath.', ex.message
}
}
@Test
void testCompareAndSetFalse() {
Deserializer deserializer = createMock(Deserializer)
def locator = new RuntimeClasspathDeserializerLocator() {
@Override
protected boolean compareAndSet(Deserializer d) {
RuntimeClasspathDeserializerLocator.DESERIALIZER.set(deserializer)
return false
}
}
def returned = locator.getInstance()
assertSame deserializer, returned
}
@Test(expected = IllegalStateException)
void testLocateReturnsNull() {
def locator = new RuntimeClasspathDeserializerLocator() {
@Override
protected Deserializer locate() {
return null
}
}
locator.getInstance()
}
@Test(expected = IllegalStateException)
void testCompareAndSetFalseWithNullReturn() {
def locator = new RuntimeClasspathDeserializerLocator() {
@Override
protected boolean compareAndSet(Deserializer d) {
return false
}
}
locator.getInstance()
}
@Test
void testJackson() {
def deserializer = new RuntimeClasspathDeserializerLocator().getInstance()
assertTrue deserializer instanceof JacksonDeserializer
}
@Test
void testOrgJson() {
def locator = new RuntimeClasspathDeserializerLocator() {
@Override
protected boolean isAvailable(String fqcn) {
if (ObjectMapper.class.getName().equals(fqcn)) {
return false; //skip it to allow the OrgJson impl to be created
}
return super.isAvailable(fqcn)
}
}
def deserializer = locator.getInstance()
assertTrue deserializer instanceof OrgJsonDeserializer
}
}

View File

@ -0,0 +1,99 @@
package io.jsonwebtoken.io.impl
import com.fasterxml.jackson.databind.ObjectMapper
import io.jsonwebtoken.io.Serializer
import io.jsonwebtoken.io.impl.jackson.JacksonSerializer
import io.jsonwebtoken.io.impl.orgjson.OrgJsonSerializer
import org.junit.After
import org.junit.Before
import org.junit.Test
import static org.easymock.EasyMock.createMock
import static org.junit.Assert.*
class RuntimeClasspathSerializerLocatorTest {
@Before
void setUp() {
RuntimeClasspathSerializerLocator.SERIALIZER.set(null)
}
@After
void teardown() {
RuntimeClasspathSerializerLocator.SERIALIZER.set(null)
}
@Test
void testClassIsNotAvailable() {
def locator = new RuntimeClasspathSerializerLocator() {
@Override
protected boolean isAvailable(String fqcn) {
return false
}
}
try {
locator.getInstance()
} catch (Exception ex) {
assertEquals 'Unable to discover any JSON Serializer implementations on the classpath.', ex.message
}
}
@Test
void testCompareAndSetFalse() {
Serializer serializer = createMock(Serializer)
def locator = new RuntimeClasspathSerializerLocator() {
@Override
protected boolean compareAndSet(Serializer s) {
RuntimeClasspathSerializerLocator.SERIALIZER.set(serializer)
return false
}
}
def returned = locator.getInstance()
assertSame serializer, returned
}
@Test(expected = IllegalStateException)
void testLocateReturnsNull() {
def locator = new RuntimeClasspathSerializerLocator() {
@Override
protected Serializer<Object> locate() {
return null
}
}
locator.getInstance()
}
@Test(expected = IllegalStateException)
void testCompareAndSetFalseWithNullReturn() {
def locator = new RuntimeClasspathSerializerLocator() {
@Override
protected boolean compareAndSet(Serializer<Object> s) {
return false
}
}
locator.getInstance()
}
@Test
void testJackson() {
def serializer = new RuntimeClasspathSerializerLocator().getInstance()
assertTrue serializer instanceof JacksonSerializer
}
@Test
void testOrgJson() {
def locator = new RuntimeClasspathSerializerLocator() {
@Override
protected boolean isAvailable(String fqcn) {
if (ObjectMapper.class.getName().equals(fqcn)) {
return false //skip it to allow the OrgJson impl to be created
}
return super.isAvailable(fqcn)
}
}
def serializer = locator.getInstance()
assertTrue serializer instanceof OrgJsonSerializer
}
}

View File

@ -0,0 +1,68 @@
package io.jsonwebtoken.io.impl.jackson
import com.fasterxml.jackson.databind.ObjectMapper
import io.jsonwebtoken.io.DeserializationException
import io.jsonwebtoken.lang.Strings
import org.junit.Test
import static org.easymock.EasyMock.createMock
import static org.easymock.EasyMock.expect
import static org.easymock.EasyMock.replay
import static org.easymock.EasyMock.verify
import static org.junit.Assert.*
class JacksonDeserializerTest {
@Test
void testDefaultConstructor() {
def deserializer = new JacksonDeserializer()
assertNotNull deserializer.objectMapper
}
@Test
void testObjectMapperConstructor() {
def customOM = new ObjectMapper()
def deserializer = new JacksonDeserializer(customOM)
assertSame customOM, deserializer.objectMapper
}
@Test(expected = IllegalArgumentException)
void testObjectMapperConstructorWithNullArgument() {
new JacksonDeserializer<>(null)
}
@Test
void testDeserialize() {
byte[] serialized = '{"hello":"世界"}'.getBytes(Strings.UTF_8)
def expected = [hello: '世界']
def result = new JacksonDeserializer().deserialize(serialized)
assertEquals expected, result
}
@Test
void testDeserializeFailsWithJsonProcessingException() {
def ex = createMock(IOException)
expect(ex.getMessage()).andReturn('foo')
def deserializer = new JacksonDeserializer() {
@Override
protected Object readValue(byte[] bytes) throws IOException {
throw ex
}
}
replay ex
try {
deserializer.deserialize('{"hello":"世界"}'.getBytes(Strings.UTF_8))
fail()
} catch (DeserializationException se) {
assertEquals 'Unable to deserialize bytes into a java.util.Map instance: foo', se.getMessage()
assertSame ex, se.getCause()
}
verify ex
}
}

View File

@ -0,0 +1,107 @@
package io.jsonwebtoken.io.impl.jackson
import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.ObjectMapper
import io.jsonwebtoken.codec.Encoder
import io.jsonwebtoken.io.SerializationException
import io.jsonwebtoken.lang.Strings
import org.junit.Test
import static org.easymock.EasyMock.createMock
import static org.easymock.EasyMock.expect
import static org.easymock.EasyMock.replay
import static org.easymock.EasyMock.verify
import static org.junit.Assert.*
class JacksonSerializerTest {
@Test
void testDefaultConstructor() {
def serializer = new JacksonSerializer()
assertNotNull serializer.objectMapper
}
@Test
void testObjectMapperConstructor() {
def customOM = new ObjectMapper()
def serializer = new JacksonSerializer<>(customOM)
assertSame customOM, serializer.objectMapper
}
@Test(expected = IllegalArgumentException)
void testObjectMapperConstructorWithNullArgument() {
new JacksonSerializer<>(null)
}
@Test
void testByte() {
byte[] expected = "120".getBytes(Strings.UTF_8) //ascii("x") = 120
byte[] bytes = "x".getBytes(Strings.UTF_8)
byte[] result = new JacksonSerializer().serialize(bytes[0]) //single byte
assertTrue Arrays.equals(expected, result)
}
@Test
void testByteArray() { //expect Base64 string by default:
byte[] bytes = "hi".getBytes(Strings.UTF_8)
String encoded = Encoder.BASE64.encode(bytes)
String expected = "\"$encoded\"" as String
byte[] result = new JacksonSerializer().serialize(bytes)
assertEquals expected, new String(result, Strings.UTF_8)
}
@Test
void testEmptyByteArray() { //expect Base64 string by default:
byte[] bytes = new byte[0]
String encoded = Encoder.BASE64.encode(bytes)
String expected = "\"$encoded\"" as String
byte[] result = new JacksonSerializer().serialize(bytes)
assertEquals expected, new String(result, Strings.UTF_8)
}
@Test
void testChar() { //expect Base64 string by default:
byte[] result = new JacksonSerializer().serialize('h' as char)
assertEquals "\"h\"", new String(result, Strings.UTF_8)
}
@Test
void testCharArray() { //expect Base64 string by default:
byte[] result = new JacksonSerializer().serialize("hi".toCharArray())
assertEquals "\"hi\"", new String(result, Strings.UTF_8)
}
@Test
void testSerialize() {
byte[] expected = '{"hello":"世界"}'.getBytes(Strings.UTF_8)
byte[] result = new JacksonSerializer().serialize([hello: '世界'])
assertTrue Arrays.equals(expected, result)
}
@Test
void testSerializeFailsWithJsonProcessingException() {
def ex = createMock(JsonProcessingException)
expect(ex.getMessage()).andReturn('foo')
def serializer = new JacksonSerializer() {
@Override
protected byte[] writeValueAsBytes(Object o) throws JsonProcessingException {
throw ex
}
}
replay ex
try {
serializer.serialize([hello: 'world'])
fail()
} catch (SerializationException se) {
assertEquals 'Unable to serialize object: foo', se.getMessage()
assertSame ex, se.getCause()
}
verify ex
}
}

View File

@ -0,0 +1,147 @@
package io.jsonwebtoken.io.impl.orgjson
import io.jsonwebtoken.io.DeserializationException
import io.jsonwebtoken.lang.Strings
import org.junit.Test
import static org.junit.Assert.*
class OrgJsonDeserializerTest {
@Test(expected=IllegalArgumentException)
void testNullArgument() {
def d = new OrgJsonDeserializer()
d.deserialize(null)
}
@Test(expected = DeserializationException)
void testEmptyByteArray() {
def d = new OrgJsonDeserializer()
d.deserialize(new byte[0])
}
@Test(expected = DeserializationException)
void testInvalidJson() {
def d = new OrgJsonDeserializer()
d.deserialize('"'.getBytes(Strings.UTF_8))
}
@Test
void testLiteralNull() {
def d = new OrgJsonDeserializer();
def b = 'null'.getBytes(Strings.UTF_8)
def value = d.deserialize(b)
assertNull value
}
@Test
void testLiteralTrue() {
def d = new OrgJsonDeserializer();
def b = 'true'.getBytes(Strings.UTF_8)
def value = d.deserialize(b)
assertEquals Boolean.TRUE, value
}
@Test
void testLiteralFalse() {
def d = new OrgJsonDeserializer();
def b = 'false'.getBytes(Strings.UTF_8)
def value = d.deserialize(b)
assertEquals Boolean.FALSE, value
}
@Test
void testLiteralInteger() {
def d = new OrgJsonDeserializer();
def b = '1'.getBytes(Strings.UTF_8)
def value = d.deserialize(b)
assert value instanceof Integer
assertEquals 1 as Integer, value
}
@Test
void testLiteralDecimal() {
def d = new OrgJsonDeserializer();
def b = '3.14159'.getBytes(Strings.UTF_8)
def value = d.deserialize(b)
assert value instanceof Double
assertEquals 3.14159 as Double, value, 0d
}
@Test
void testEmptyArray() {
def d = new OrgJsonDeserializer();
def b = '[]'.getBytes(Strings.UTF_8)
def value = d.deserialize(b)
assert value instanceof List
assertEquals 0, value.size()
}
@Test
void testSimpleArray() {
def d = new OrgJsonDeserializer();
def b = '[1, 2]'.getBytes(Strings.UTF_8)
def value = d.deserialize(b)
assert value instanceof List
def expected = [1, 2]
assertEquals expected, value
}
@Test
void testArrayWithNullElements() {
def d = new OrgJsonDeserializer();
def b = '[1, null, 3]'.getBytes(Strings.UTF_8)
def value = d.deserialize(b)
assert value instanceof List
def expected = [1, null, 3]
assertEquals expected, value
}
@Test
void testEmptyObject() {
def d = new OrgJsonDeserializer();
def b = '{}'.getBytes(Strings.UTF_8)
def value = d.deserialize(b)
assert value instanceof Map
assertEquals 0, value.size()
}
@Test
void testSimpleObject() {
def d = new OrgJsonDeserializer();
def b = '{"hello": "世界"}'.getBytes(Strings.UTF_8)
def value = d.deserialize(b)
assert value instanceof Map
def expected = [hello: '世界']
assertEquals expected, value
}
@Test
void testObjectWithKeyHavingNullValue() {
def d = new OrgJsonDeserializer();
def b = '{"hello": "世界", "test": null}'.getBytes(Strings.UTF_8)
def value = d.deserialize(b)
assert value instanceof Map
def expected = [hello: '世界', test: null]
assertEquals expected, value
}
@Test
void testObjectWithKeyHavingArrayValue() {
def d = new OrgJsonDeserializer();
def b = '{"hello": "世界", "test": [1, 2]}'.getBytes(Strings.UTF_8)
def value = d.deserialize(b)
assert value instanceof Map
def expected = [hello: '世界', test: [1, 2]]
assertEquals expected, value
}
@Test
void testObjectWithKeyHavingObjectValue() {
def d = new OrgJsonDeserializer();
def b = '{"hello": "世界", "test": {"foo": "bar"}}'.getBytes(Strings.UTF_8)
def value = d.deserialize(b)
assert value instanceof Map
def expected = [hello: '世界', test: [foo: 'bar']]
assertEquals expected, value
}
}

View File

@ -0,0 +1,257 @@
package io.jsonwebtoken.io.impl.orgjson
import io.jsonwebtoken.SignatureAlgorithm
import io.jsonwebtoken.codec.Encoder
import io.jsonwebtoken.io.SerializationException
import io.jsonwebtoken.lang.DateFormats
import io.jsonwebtoken.lang.Strings
import org.json.JSONObject
import org.json.JSONString
import org.junit.Before
import org.junit.Test
import static org.junit.Assert.*
class OrgJsonSerializerTest {
private OrgJsonSerializer s
@Before
void setUp() {
s = new OrgJsonSerializer()
}
private String ser(Object o) {
byte[] bytes = s.serialize(o)
return new String(bytes, Strings.UTF_8)
}
@Test(expected = SerializationException)
void testInvalidArgument() {
s.serialize(new Object())
}
@Test
void testToBytesFailure() {
final IllegalArgumentException iae = new IllegalArgumentException("foo")
s = new OrgJsonSerializer() {
@Override
protected byte[] toBytes(Object o) {
throw iae
}
}
try {
s.serialize("hello")
fail()
} catch (SerializationException se) {
assertTrue se.getMessage().endsWith(iae.getMessage())
assertSame iae, se.getCause()
}
}
@Test
void testNull() {
assertEquals 'null', ser(null)
}
@Test
void testJSONObjectNull() {
assertEquals 'null', ser(JSONObject.NULL)
}
@Test
void testJSONString() {
def jsonString = new JSONString() {
@Override
String toJSONString() {
return '"foo"'
}
}
assertEquals '"foo"', ser(jsonString)
}
@Test
void testTrue() {
assertEquals 'true', ser(Boolean.TRUE)
}
@Test
void testFalse() {
assertEquals 'false', ser(Boolean.FALSE)
}
@Test
void testByte() {
assertEquals '120', ser("x".getBytes(Strings.UTF_8)[0]) //ascii("x") == 120
}
@Test
void testByteArray() { //expect Base64 string by default:
byte[] bytes = "hi".getBytes(Strings.UTF_8)
String encoded = Encoder.BASE64.encode(bytes)
String expected = "\"$encoded\"" as String
assertEquals expected, ser(bytes)
}
@Test
void testEmptyByteArray() { //base64 --> zero bytes == zero-length string:
assertEquals "\"\"", ser(new byte[0])
}
@Test
void testChar() {
assertEquals "\"h\"", ser('h' as char)
}
@Test
void testCharArray() {
assertEquals "\"hi\"", ser("hi".toCharArray())
}
@Test
void testEmptyCharArray() { //no chars == empty string:
assertEquals "\"\"", ser(new char[0])
}
@Test
void testShort() {
assertEquals '8', ser(8 as short)
}
@Test
void testInteger() {
assertEquals '1', ser(1 as Integer)
}
@Test
void testLong() {
assertEquals '42', ser(42 as Long)
}
@Test
void testBigInteger() {
assertEquals '42', ser(BigInteger.valueOf(42 as Long))
}
@Test
void testFloat() {
assertEquals '3.14159', ser(3.14159 as Float)
}
@Test
void testDouble() {
assertEquals '3.14159', ser(3.14159 as Double)
}
@Test
void testBigDecimal() {
assertEquals '3.14159', ser(BigDecimal.valueOf(3.14159 as Double))
}
@Test
void testEnum() {
assertEquals '"HS256"', ser(SignatureAlgorithm.HS256)
}
@Test
void testEmptyString() {
assertEquals '""', ser('')
}
@Test
void testWhitespaceString() {
String value = " \n\r\t "
assertEquals '" \\n\\r\\t "' as String, ser(value)
}
@Test
void testSimpleString() {
assertEquals '"hello 世界"', ser('hello 世界')
}
@Test
void testDate() {
Date now = new Date()
String formatted = DateFormats.formatIso8601(now)
assertEquals "\"$formatted\"" as String, ser(now)
}
@Test
void testCalendar() {
def cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
def now = cal.getTime()
String formatted = DateFormats.formatIso8601(now)
assertEquals "\"$formatted\"" as String, ser(cal)
}
@Test
void testSimpleIntArray() {
assertEquals '[1,2]', ser( [1, 2] as int[] )
}
@Test
void testIntegerArrayWithNullElements() {
assertEquals '[1,null]', ser( [1, null] as Integer[] )
}
@Test
void testIntegerList() {
assertEquals '[1,2]', ser( [1, 2] as List)
}
@Test
void testEmptyObject() {
assertEquals '{}', ser([:])
}
@Test
void testSimpleObject() {
assertEquals '{"hello":"世界"}', ser([hello: '世界'])
}
@Test
void testObjectWithKeyHavingNullValue() {
//depending on the test platform, and that JSON doesn't require members to be ordered, either of the
//two strings are fine (they're the same data, just the member order is different):
String acceptable1 = '{"hello":"世界","test":null}'
String acceptable2 = '{"test":null,"hello":"世界"}'
String result = ser([test: null, hello: '世界'])
assertTrue acceptable1.equals(result) || acceptable2.equals(result)
}
@Test
void testObjectWithKeyHavingArrayValue() {
//depending on the test platform, and that JSON doesn't require members to be ordered, either of the
//two strings are fine (they're the same data, just the member order is different):
String acceptable1 = '{"test":[1,2],"hello":"世界"}'
String acceptable2 = '{"hello":"世界","test":[1,2]}'
String result = ser([test: [1, 2], hello: '世界'])
assertTrue acceptable1.equals(result) || acceptable2.equals(result)
}
@Test
void testObjectWithKeyHavingObjectValue() {
//depending on the test platform, and that JSON doesn't require members to be ordered, either of the
//two strings are fine (they're the same data, just the member order is different):
String acceptable1 = '{"test":{"foo":"bar"},"hello":"世界"}'
String acceptable2 = '{"hello":"世界","test":{"foo":"bar"}}'
String result = ser([test: [foo: 'bar'], hello: '世界'])
assertTrue acceptable1.equals(result) || acceptable2.equals(result)
}
@Test
void testListWithNullElements() {
assertEquals '[1,null,null]', ser( [1, null, null] as List)
}
@Test
void testListWithSingleNullElement() {
assertEquals '[null]', ser([null] as List)
}
@Test
void testListWithNestedObject() {
assertEquals '[1,null,{"hello":"世界"}]', ser([1, null, [hello: '世界']])
}
}