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

View File

@ -16,6 +16,7 @@
package io.jsonwebtoken; package io.jsonwebtoken;
import io.jsonwebtoken.codec.Encoder; import io.jsonwebtoken.codec.Encoder;
import io.jsonwebtoken.io.Serializer;
import java.security.Key; import java.security.Key;
import java.util.Date; import java.util.Date;
@ -426,6 +427,20 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
*/ */
JwtBuilder base64UrlEncodeWith(Encoder<byte[], String> base64UrlEncoder); 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 * 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> * <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.codec.Decoder;
import io.jsonwebtoken.impl.DefaultClock; import io.jsonwebtoken.impl.DefaultClock;
import io.jsonwebtoken.io.Deserializer;
import java.security.Key; import java.security.Key;
import java.util.Date; 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. * 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); 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} * Returns {@code true} if the specified JWT compact string represents a signed JWT (aka a 'JWS'), {@code false}
* otherwise. * otherwise.

View File

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

View File

@ -15,8 +15,6 @@
*/ */
package io.jsonwebtoken.impl; package io.jsonwebtoken.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import io.jsonwebtoken.CompressionCodec; import io.jsonwebtoken.CompressionCodec;
import io.jsonwebtoken.Header; import io.jsonwebtoken.Header;
@ -29,7 +27,11 @@ import io.jsonwebtoken.codec.Decoder;
import io.jsonwebtoken.codec.Encoder; import io.jsonwebtoken.codec.Encoder;
import io.jsonwebtoken.impl.crypto.DefaultJwtSigner; import io.jsonwebtoken.impl.crypto.DefaultJwtSigner;
import io.jsonwebtoken.impl.crypto.JwtSigner; 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.Assert;
import io.jsonwebtoken.lang.Classes;
import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Collections;
import io.jsonwebtoken.lang.Strings; import io.jsonwebtoken.lang.Strings;
@ -40,8 +42,6 @@ import java.util.Map;
public class DefaultJwtBuilder implements JwtBuilder { public class DefaultJwtBuilder implements JwtBuilder {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private Header header; private Header header;
private Claims claims; private Claims claims;
private String payload; private String payload;
@ -49,10 +49,19 @@ public class DefaultJwtBuilder implements JwtBuilder {
private SignatureAlgorithm algorithm; private SignatureAlgorithm algorithm;
private Key key; private Key key;
private Serializer<Map<String,?>> serializer;
private Encoder<byte[], String> base64UrlEncoder = Encoder.BASE64URL; private Encoder<byte[], String> base64UrlEncoder = Encoder.BASE64URL;
private CompressionCodec compressionCodec; private CompressionCodec compressionCodec;
@Override
public JwtBuilder serializeToJsonWith(Serializer<Map<String,?>> serializer) {
Assert.notNull(serializer, "Serializer cannot be null.");
this.serializer = serializer;
return this;
}
@Override @Override
public JwtBuilder base64UrlEncodeWith(Encoder<byte[], String> base64UrlEncoder) { public JwtBuilder base64UrlEncodeWith(Encoder<byte[], String> base64UrlEncoder) {
Assert.notNull(base64UrlEncoder, "base64UrlEncoder cannot be null."); Assert.notNull(base64UrlEncoder, "base64UrlEncoder cannot be null.");
@ -270,6 +279,14 @@ public class DefaultJwtBuilder implements JwtBuilder {
@Override @Override
public String compact() { 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)) { if (payload == null && Collections.isEmpty(claims)) {
throw new IllegalStateException("Either 'payload' or 'claims' must be specified."); throw new IllegalStateException("Either 'payload' or 'claims' must be specified.");
} }
@ -304,8 +321,8 @@ public class DefaultJwtBuilder implements JwtBuilder {
byte[] bytes; byte[] bytes;
try { try {
bytes = this.payload != null ? payload.getBytes(Strings.UTF_8) : toJson(claims); bytes = this.payload != null ? payload.getBytes(Strings.UTF_8) : toJson(claims);
} catch (JsonProcessingException e) { } catch (SerializationException e) {
throw new IllegalArgumentException("Unable to serialize claims object to json."); throw new IllegalArgumentException("Unable to serialize claims object to json: " + e.getMessage(), e);
} }
if (compressionCodec != null) { if (compressionCodec != null) {
@ -339,18 +356,25 @@ public class DefaultJwtBuilder implements JwtBuilder {
return new DefaultJwtSigner(alg, key, base64UrlEncoder); return new DefaultJwtSigner(alg, key, base64UrlEncoder);
} }
@Deprecated // remove before 1.0 - call the serializer and base64UrlEncoder directly
protected String base64UrlEncode(Object o, String errMsg) { protected String base64UrlEncode(Object o, String errMsg) {
Assert.isInstanceOf(Map.class, o, "object argument must be a map.");
Map m = (Map)o;
byte[] bytes; byte[] bytes;
try { try {
bytes = toJson(o); bytes = toJson(m);
} catch (JsonProcessingException e) { } catch (SerializationException e) {
throw new IllegalStateException(errMsg, e); throw new IllegalStateException(errMsg, e);
} }
return base64UrlEncoder.encode(bytes); return base64UrlEncoder.encode(bytes);
} }
protected byte[] toJson(Object object) throws JsonProcessingException { @SuppressWarnings("unchecked")
return OBJECT_MAPPER.writeValueAsBytes(object); @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; package io.jsonwebtoken.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.ClaimJwtException; import io.jsonwebtoken.ClaimJwtException;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Clock; import io.jsonwebtoken.Clock;
@ -42,26 +41,25 @@ import io.jsonwebtoken.codec.Decoder;
import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver; import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver;
import io.jsonwebtoken.impl.crypto.DefaultJwtSignatureValidator; import io.jsonwebtoken.impl.crypto.DefaultJwtSignatureValidator;
import io.jsonwebtoken.impl.crypto.JwtSignatureValidator; 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.Assert;
import io.jsonwebtoken.lang.Classes;
import io.jsonwebtoken.lang.DateFormats;
import io.jsonwebtoken.lang.Objects; import io.jsonwebtoken.lang.Objects;
import io.jsonwebtoken.lang.Strings; import io.jsonwebtoken.lang.Strings;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.security.Key; import java.security.Key;
import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.Map; import java.util.Map;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public class DefaultJwtParser implements JwtParser { 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 static final int MILLISECONDS_PER_SECOND = 1000;
private ObjectMapper objectMapper = new ObjectMapper();
private byte[] keyBytes; private byte[] keyBytes;
private Key key; private Key key;
@ -72,12 +70,21 @@ public class DefaultJwtParser implements JwtParser {
private Decoder<String, byte[]> base64UrlDecoder = Decoder.BASE64URL; private Decoder<String, byte[]> base64UrlDecoder = Decoder.BASE64URL;
private Deserializer<Map<String, ?>> deserializer;
private Claims expectedClaims = new DefaultClaims(); private Claims expectedClaims = new DefaultClaims();
private Clock clock = DefaultClock.INSTANCE; private Clock clock = DefaultClock.INSTANCE;
private long allowedClockSkewMillis = 0; 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 @Override
public JwtParser base64UrlDecodeWith(Decoder<String, byte[]> base64UrlDecoder) { public JwtParser base64UrlDecodeWith(Decoder<String, byte[]> base64UrlDecoder) {
Assert.notNull(base64UrlDecoder, "base64UrlDecoder cannot be null."); Assert.notNull(base64UrlDecoder, "base64UrlDecoder cannot be null.");
@ -210,6 +217,13 @@ public class DefaultJwtParser implements JwtParser {
@Override @Override
public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, SignatureException { 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."); Assert.hasText(jwt, "JWT String argument cannot be null or empty.");
String base64UrlEncodedHeader = null; String base64UrlEncodedHeader = null;
@ -260,7 +274,7 @@ public class DefaultJwtParser implements JwtParser {
if (base64UrlEncodedHeader != null) { if (base64UrlEncodedHeader != null) {
byte[] bytes = base64UrlDecoder.decode(base64UrlEncodedHeader); byte[] bytes = base64UrlDecoder.decode(base64UrlEncodedHeader);
String origValue = new String(bytes, Strings.UTF_8); 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) { if (base64UrlEncodedDigest != null) {
header = new DefaultJwsHeader(m); header = new DefaultJwsHeader(m);
@ -281,7 +295,7 @@ public class DefaultJwtParser implements JwtParser {
Claims claims = null; Claims claims = null;
if (payload.charAt(0) == '{' && payload.charAt(payload.length() - 1) == '}') { //likely to be json, parse it: 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); claims = new DefaultClaims(claimsMap);
} }
@ -369,8 +383,6 @@ public class DefaultJwtParser implements JwtParser {
//since 0.3: //since 0.3:
if (claims != null) { if (claims != null) {
SimpleDateFormat sdf;
final Date now = this.clock.now(); final Date now = this.clock.now();
long nowTime = now.getTime(); long nowTime = now.getTime();
@ -382,9 +394,8 @@ public class DefaultJwtParser implements JwtParser {
long maxTime = nowTime - this.allowedClockSkewMillis; long maxTime = nowTime - this.allowedClockSkewMillis;
Date max = allowSkew ? new Date(maxTime) : now; Date max = allowSkew ? new Date(maxTime) : now;
if (max.after(exp)) { if (max.after(exp)) {
sdf = new SimpleDateFormat(ISO_8601_FORMAT); String expVal = DateFormats.formatIso8601(exp, false);
String expVal = sdf.format(exp); String nowVal = DateFormats.formatIso8601(now, false);
String nowVal = sdf.format(now);
long differenceMillis = maxTime - exp.getTime(); long differenceMillis = maxTime - exp.getTime();
@ -403,9 +414,8 @@ public class DefaultJwtParser implements JwtParser {
long minTime = nowTime + this.allowedClockSkewMillis; long minTime = nowTime + this.allowedClockSkewMillis;
Date min = allowSkew ? new Date(minTime) : now; Date min = allowSkew ? new Date(minTime) : now;
if (min.before(nbf)) { if (min.before(nbf)) {
sdf = new SimpleDateFormat(ISO_8601_FORMAT); String nbfVal = DateFormats.formatIso8601(nbf, false);
String nbfVal = sdf.format(nbf); String nowVal = DateFormats.formatIso8601(now, false);
String nowVal = sdf.format(now);
long differenceMillis = nbf.getTime() - minTime; long differenceMillis = nbf.getTime() - minTime;
@ -423,27 +433,37 @@ public class DefaultJwtParser implements JwtParser {
Object body = claims != null ? claims : payload; Object body = claims != null ? claims : payload;
if (base64UrlEncodedDigest != null) { if (base64UrlEncodedDigest != null) {
return new DefaultJws<Object>((JwsHeader) header, body, base64UrlEncodedDigest); return new DefaultJws<>((JwsHeader) header, body, base64UrlEncodedDigest);
} else { } 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) { private void validateExpectedClaims(Header header, Claims claims) {
for (String expectedClaimName : expectedClaims.keySet()) { for (String expectedClaimName : expectedClaims.keySet()) {
Object expectedClaimValue = expectedClaims.get(expectedClaimName); Object expectedClaimValue = normalize(expectedClaims.get(expectedClaimName));
Object actualClaimValue = claims.get(expectedClaimName); Object actualClaimValue = normalize(claims.get(expectedClaimName));
if (Claims.ISSUED_AT.equals(expectedClaimName) || Claims.EXPIRATION.equals(expectedClaimName) || if (expectedClaimValue instanceof Date) {
Claims.NOT_BEFORE.equals(expectedClaimName)) { try {
expectedClaimValue = expectedClaims.get(expectedClaimName, Date.class);
actualClaimValue = claims.get(expectedClaimName, Date.class); actualClaimValue = claims.get(expectedClaimName, Date.class);
} catch (Exception e) {
} else if (expectedClaimValue instanceof Date && actualClaimValue instanceof Long) { 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;
actualClaimValue = new Date((Long) actualClaimValue); throw new IncorrectClaimException(header, claims, msg);
}
} }
InvalidClaimException invalidClaimException = null; InvalidClaimException invalidClaimException = null;
@ -553,10 +573,11 @@ public class DefaultJwtParser implements JwtParser {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
protected Map<String, Object> readValue(String val) { protected Map<String, ?> readValue(String val) {
try { try {
return objectMapper.readValue(val, Map.class); byte[] bytes = val.getBytes(Strings.UTF_8);
} catch (IOException e) { return deserializer.deserialize(bytes);
} catch (DeserializationException e) {
throw new MalformedJwtException("Unable to read JSON value: " + val, e); throw new MalformedJwtException("Unable to read JSON value: " + val, e);
} }
} }

View File

@ -16,7 +16,10 @@
package io.jsonwebtoken.impl; package io.jsonwebtoken.impl;
import io.jsonwebtoken.lang.Assert; 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.Collection;
import java.util.Date; import java.util.Date;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@ -46,24 +49,56 @@ public class JwtMap implements Map<String,Object> {
return null; return null;
} else if (v instanceof Date) { } else if (v instanceof Date) {
return (Date) v; 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) { } else if (v instanceof Number) {
// https://github.com/jwtk/jjwt/issues/122: // https://github.com/jwtk/jjwt/issues/122:
// The JWT RFC *mandates* NumericDate values are represented as seconds. // The JWT RFC *mandates* NumericDate values are represented as seconds.
// Because Because java.util.Date requires milliseconds, we need to multiply by 1000: // Because Because java.util.Date requires milliseconds, we need to multiply by 1000:
long seconds = ((Number) v).longValue(); long seconds = ((Number) v).longValue();
long millis = seconds * 1000; v = seconds * 1000;
return new Date(millis);
} else if (v instanceof String) { } else if (v instanceof String) {
// https://github.com/jwtk/jjwt/issues/122 // https://github.com/jwtk/jjwt/issues/122
// The JWT RFC *mandates* NumericDate values are represented as seconds. // The JWT RFC *mandates* NumericDate values are represented as seconds.
// Because Because java.util.Date requires milliseconds, we need to multiply by 1000: // Because Because java.util.Date requires milliseconds, we need to multiply by 1000:
try {
long seconds = Long.parseLong((String) v); long seconds = Long.parseLong((String) v);
long millis = seconds * 1000; v = seconds * 1000;
return new Date(millis); } catch (NumberFormatException ignored) {
} else {
throw new IllegalStateException("Cannot convert '" + name + "' value [" + v + "] to Date instance.");
} }
} }
//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) { protected void setValue(String name, Object v) {
if (v == null) { if (v == null) {
@ -73,12 +108,17 @@ public class JwtMap implements Map<String,Object> {
} }
} }
protected Date getDate(String name) { @Deprecated //remove just before 1.0.0
Object v = map.get(name); protected void setDate(String name, Date d) {
return toDate(v, name); 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) { if (d == null) {
map.remove(name); map.remove(name);
} else { } 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 * @return the located class
* @throws UnknownClassException if the class cannot be found. * @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); Class clazz = THREAD_CL_ACCESSOR.loadClass(fqcn);
@ -132,13 +133,13 @@ public final class Classes {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public static Object newInstance(String fqcn) { public static <T> T newInstance(String fqcn) {
return newInstance(forName(fqcn)); return (T)newInstance(forName(fqcn));
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public static Object newInstance(String fqcn, Object... args) { public static <T> T newInstance(String fqcn, Object... args) {
return newInstance(forName(fqcn), args); return (T)newInstance(forName(fqcn), args);
} }
public static <T> T newInstance(Class<T> clazz) { 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 javax.crypto.spec.SecretKeySpec
import java.security.SecureRandom import java.security.SecureRandom
import static org.junit.Assert.*
import static ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE import static ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE
import static ClaimJwtException.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE import static ClaimJwtException.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE
import static org.junit.Assert.*
class JwtParserTest { 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() { protected static byte[] randomKey() {
//create random signing key for testing: //create random signing key for testing:
@ -826,15 +826,11 @@ class JwtParserTest {
byte[] key = randomKey() byte[] key = randomKey()
// not setting expected claim name in JWT // not setting expected claim name in JWT
String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key).setIssuer('Dummy').compact()
setIssuer('Dummy').
compact()
try { try {
// expecting null claim name, but with value // expecting null claim name, but with value
Jwt<Header, Claims> jwt = Jwts.parser().setSigningKey(key). Jwts.parser().setSigningKey(key).require(null, expectedClaimValue).parseClaimsJws(compact)
require(null, expectedClaimValue).
parseClaimsJws(compact)
fail() fail()
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
assertEquals( assertEquals(
@ -876,9 +872,7 @@ class JwtParserTest {
byte[] key = randomKey() byte[] key = randomKey()
// not setting expected claim name in JWT // not setting expected claim name in JWT
String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key).setIssuer('Dummy').compact()
setIssuer('Dummy').
compact()
try { try {
// expecting claim name, but with null value // expecting claim name, but with null value
@ -964,6 +958,7 @@ class JwtParserTest {
@Test @Test
void testParseRequireIssuedAt_Success() { void testParseRequireIssuedAt_Success() {
def issuedAt = new Date(System.currentTimeMillis()) def issuedAt = new Date(System.currentTimeMillis())
byte[] key = randomKey() byte[] key = randomKey()
@ -982,7 +977,7 @@ class JwtParserTest {
assertEquals jwt.getBody().getIssuedAt().getTime(), issuedAtMillis, 0 assertEquals jwt.getBody().getIssuedAt().getTime(), issuedAtMillis, 0
} }
@Test @Test(expected = IncorrectClaimException)
void testParseRequireIssuedAt_Incorrect_Fail() { void testParseRequireIssuedAt_Incorrect_Fail() {
def goodIssuedAt = new Date(System.currentTimeMillis()) def goodIssuedAt = new Date(System.currentTimeMillis())
def badIssuedAt = new Date(System.currentTimeMillis() - 10000) def badIssuedAt = new Date(System.currentTimeMillis() - 10000)
@ -993,20 +988,12 @@ class JwtParserTest {
setIssuedAt(badIssuedAt). setIssuedAt(badIssuedAt).
compact() compact()
try {
Jwts.parser().setSigningKey(key). Jwts.parser().setSigningKey(key).
requireIssuedAt(goodIssuedAt). requireIssuedAt(goodIssuedAt).
parseClaimsJws(compact) 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() { void testParseRequireIssuedAt_Missing_Fail() {
def issuedAt = new Date(System.currentTimeMillis() - 10000) def issuedAt = new Date(System.currentTimeMillis() - 10000)
@ -1016,17 +1003,9 @@ class JwtParserTest {
setSubject("Dummy"). setSubject("Dummy").
compact() compact()
try {
Jwts.parser().setSigningKey(key). Jwts.parser().setSigningKey(key).
requireIssuedAt(issuedAt). requireIssuedAt(issuedAt).
parseClaimsJws(compact) parseClaimsJws(compact)
fail()
} catch(MissingClaimException e) {
assertEquals(
String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.ISSUED_AT, issuedAt),
e.getMessage()
)
}
} }
@Test @Test
@ -1306,7 +1285,7 @@ class JwtParserTest {
assertEquals jwt.getBody().getExpiration().getTime(), expirationMillis, 0 assertEquals jwt.getBody().getExpiration().getTime(), expirationMillis, 0
} }
@Test @Test(expected = IncorrectClaimException)
void testParseRequireExpirationAt_Incorrect_Fail() { void testParseRequireExpirationAt_Incorrect_Fail() {
def goodExpiration = new Date(System.currentTimeMillis() + 20000) def goodExpiration = new Date(System.currentTimeMillis() + 20000)
def badExpiration = new Date(System.currentTimeMillis() + 10000) def badExpiration = new Date(System.currentTimeMillis() + 10000)
@ -1317,20 +1296,12 @@ class JwtParserTest {
setExpiration(badExpiration). setExpiration(badExpiration).
compact() compact()
try {
Jwts.parser().setSigningKey(key). Jwts.parser().setSigningKey(key).
requireExpiration(goodExpiration). requireExpiration(goodExpiration).
parseClaimsJws(compact) 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() { void testParseRequireExpiration_Missing_Fail() {
def expiration = new Date(System.currentTimeMillis() + 10000) def expiration = new Date(System.currentTimeMillis() + 10000)
@ -1340,17 +1311,9 @@ class JwtParserTest {
setSubject("Dummy"). setSubject("Dummy").
compact() compact()
try {
Jwts.parser().setSigningKey(key). Jwts.parser().setSigningKey(key).
requireExpiration(expiration). requireExpiration(expiration).
parseClaimsJws(compact) parseClaimsJws(compact)
fail()
} catch(MissingClaimException e) {
assertEquals(
String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.EXPIRATION, expiration),
e.getMessage()
)
}
} }
@Test @Test
@ -1374,7 +1337,7 @@ class JwtParserTest {
assertEquals jwt.getBody().getNotBefore().getTime(), notBeforeMillis, 0 assertEquals jwt.getBody().getNotBefore().getTime(), notBeforeMillis, 0
} }
@Test @Test(expected = IncorrectClaimException)
void testParseRequireNotBefore_Incorrect_Fail() { void testParseRequireNotBefore_Incorrect_Fail() {
def goodNotBefore = new Date(System.currentTimeMillis() - 20000) def goodNotBefore = new Date(System.currentTimeMillis() - 20000)
def badNotBefore = new Date(System.currentTimeMillis() - 10000) def badNotBefore = new Date(System.currentTimeMillis() - 10000)
@ -1385,20 +1348,12 @@ class JwtParserTest {
setNotBefore(badNotBefore). setNotBefore(badNotBefore).
compact() compact()
try {
Jwts.parser().setSigningKey(key). Jwts.parser().setSigningKey(key).
requireNotBefore(goodNotBefore). requireNotBefore(goodNotBefore).
parseClaimsJws(compact) 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() { void testParseRequireNotBefore_Missing_Fail() {
def notBefore = new Date(System.currentTimeMillis() - 10000) def notBefore = new Date(System.currentTimeMillis() - 10000)
@ -1408,21 +1363,14 @@ class JwtParserTest {
setSubject("Dummy"). setSubject("Dummy").
compact() compact()
try {
Jwts.parser().setSigningKey(key). Jwts.parser().setSigningKey(key).
requireNotBefore(notBefore). requireNotBefore(notBefore).
parseClaimsJws(compact) parseClaimsJws(compact)
fail()
} catch(MissingClaimException e) {
assertEquals(
String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.NOT_BEFORE, notBefore),
e.getMessage()
)
}
} }
@Test @Test
void testParseRequireCustomDate_Success() { void testParseRequireCustomDate_Success() {
def aDate = new Date(System.currentTimeMillis()) def aDate = new Date(System.currentTimeMillis())
byte[] key = randomKey() byte[] key = randomKey()
@ -1438,8 +1386,33 @@ class JwtParserTest {
assertEquals jwt.getBody().get("aDate", Date.class), aDate 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 @Test
void testParseRequireCustomDate_Incorrect_Fail() { void testParseRequireCustomDate_Incorrect_Fail() {
def goodDate = new Date(System.currentTimeMillis()) def goodDate = new Date(System.currentTimeMillis())
def badDate = new Date(System.currentTimeMillis() - 10000) def badDate = new Date(System.currentTimeMillis() - 10000)
@ -1460,7 +1433,6 @@ class JwtParserTest {
e.getMessage() e.getMessage()
) )
} }
} }
@Test @Test
@ -1534,18 +1506,17 @@ class JwtParserTest {
String bad = base64Url(header) + '.' + base64Url(payload) + '.' + base64Url(badSig) + '.' + base64Url(bogus) String bad = base64Url(header) + '.' + base64Url(payload) + '.' + base64Url(badSig) + '.' + base64Url(bogus)
try { try {
Jwts.parser().setSigningKey(randomKey()).parse(bad) Jwts.parser().setSigningKey(randomKey()).parse(bad)
fail() fail()
} catch (MalformedJwtException se) { } catch (MalformedJwtException se) {
assertEquals 'JWT strings must contain exactly 2 period characters. Found: 3', se.message assertEquals 'JWT strings must contain exactly 2 period characters. Found: 3', se.message
} }
} }
@Test @Test
void testNoHeaderNoSig() { void testNoHeaderNoSig() {
String payload = '{"subject":"Joe"}' String payload = '{"subject":"Joe"}'
String jwtStr = '.' + base64Url(payload) + '.' String jwtStr = '.' + base64Url(payload) + '.'
@ -1575,16 +1546,17 @@ class JwtParserTest {
@Test @Test
void testBadHeaderSig() { void testBadHeaderSig() {
String header = '{"alg":"none"}' String header = '{"alg":"none"}'
String payload = '{"subject":"Joe"}' String payload = '{"subject":"Joe"}'
String sig = ";aklsjdf;kajsd;fkjas;dklfj" String sig = ";aklsjdf;kajsd;fkjas;dklfj"
String jwtStr = base64Url(payload) + '.' + base64Url(payload) + '.' + base64Url(sig) String jwtStr = base64Url(header) + '.' + base64Url(payload) + '.' + base64Url(sig)
try { try {
Jwt jwt = Jwts.parser().parse(jwtStr) Jwts.parser().parse(jwtStr)
fail() fail()
} catch (MalformedJwtException se) { } catch (MalformedJwtException se) {
assertEquals 'JWT string has a digest/signature, but the header does not reference a valid signature algorithm.', se.message 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 package io.jsonwebtoken
import com.fasterxml.jackson.databind.ObjectMapper
import io.jsonwebtoken.codec.Encoder import io.jsonwebtoken.codec.Encoder
import io.jsonwebtoken.impl.DefaultHeader import io.jsonwebtoken.impl.DefaultHeader
import io.jsonwebtoken.impl.DefaultJwsHeader 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.EllipticCurveProvider
import io.jsonwebtoken.impl.crypto.MacProvider import io.jsonwebtoken.impl.crypto.MacProvider
import io.jsonwebtoken.impl.crypto.RsaProvider import io.jsonwebtoken.impl.crypto.RsaProvider
import io.jsonwebtoken.io.impl.RuntimeClasspathSerializerLocator
import io.jsonwebtoken.lang.Strings import io.jsonwebtoken.lang.Strings
import org.junit.Test import org.junit.Test
@ -44,6 +44,12 @@ class JwtsTest {
return Encoder.BASE64URL.encode(bytes) 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 @Test
void testSubclass() { void testSubclass() {
new Jwts() new Jwts()
@ -617,9 +623,8 @@ class JwtsTest {
PublicKey publicKey = kp.getPublic(); PublicKey publicKey = kp.getPublic();
PrivateKey privateKey = kp.getPrivate(); PrivateKey privateKey = kp.getPrivate();
ObjectMapper om = new ObjectMapper() String header = base64Url(toJson(['alg': 'HS256']))
String header = base64Url(om.writeValueAsString(['alg': 'HS256'])) String body = base64Url(toJson('foo'))
String body = base64Url(om.writeValueAsString('foo'))
String compact = header + '.' + body + '.' String compact = header + '.' + body + '.'
// Now for the forgery: simulate an attacker using the RSA public key to sign a token, but // 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(); PublicKey publicKey = kp.getPublic();
//PrivateKey privateKey = kp.getPrivate(); //PrivateKey privateKey = kp.getPrivate();
ObjectMapper om = new ObjectMapper() String header = base64Url(toJson(['alg': 'HS256']))
String header = base64Url(om.writeValueAsString(['alg': 'HS256'])) String body = base64Url(toJson('foo'))
String body = base64Url(om.writeValueAsString('foo'))
String compact = header + '.' + body + '.' String compact = header + '.' + body + '.'
// Now for the forgery: simulate an attacker using the RSA public key to sign a token, but // 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(); PublicKey publicKey = kp.getPublic();
//PrivateKey privateKey = kp.getPrivate(); //PrivateKey privateKey = kp.getPrivate();
ObjectMapper om = new ObjectMapper() String header = base64Url(toJson(['alg': 'HS256']))
String header = base64Url(om.writeValueAsString(['alg': 'HS256'])) String body = base64Url(toJson('foo'))
String body = base64Url(om.writeValueAsString('foo'))
String compact = header + '.' + body + '.' String compact = header + '.' + body + '.'
// Now for the forgery: simulate an attacker using the Elliptic Curve public key to sign a token, but // 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 package io.jsonwebtoken.impl
import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.JsonMappingException
import io.jsonwebtoken.Jwts import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.SignatureAlgorithm
import io.jsonwebtoken.codec.Encoder import io.jsonwebtoken.codec.Encoder
import io.jsonwebtoken.codec.EncodingException import io.jsonwebtoken.codec.EncodingException
import io.jsonwebtoken.impl.compression.CompressionCodecs import io.jsonwebtoken.impl.compression.CompressionCodecs
import io.jsonwebtoken.impl.crypto.MacProvider import io.jsonwebtoken.impl.crypto.MacProvider
import io.jsonwebtoken.io.SerializationException
import io.jsonwebtoken.io.impl.orgjson.OrgJsonSerializer
import org.junit.Test import org.junit.Test
import static org.junit.Assert.* import static org.junit.Assert.*
@ -181,8 +181,8 @@ class DefaultJwtBuilderTest {
def b = new DefaultJwtBuilder() { def b = new DefaultJwtBuilder() {
@Override @Override
protected byte[] toJson(Object o) throws JsonProcessingException { protected byte[] toJson(Object o) throws SerializationException {
throw new JsonMappingException('foo') throw new SerializationException('foo', new Exception())
} }
} }
@ -192,16 +192,17 @@ class DefaultJwtBuilderTest {
} catch (IllegalStateException ise) { } catch (IllegalStateException ise) {
assertEquals ise.cause.message, 'foo' assertEquals ise.cause.message, 'foo'
} }
} }
@Test @Test
void testCompactCompressionCodecJsonProcessingException() { void testCompactCompressionCodecJsonProcessingException() {
def b = new DefaultJwtBuilder() { def b = new DefaultJwtBuilder() {
@Override @Override
protected byte[] toJson(Object o) throws JsonProcessingException { protected byte[] toJson(Object o) throws SerializationException {
if (o instanceof DefaultJwsHeader) { return super.toJson(o) } if (o instanceof DefaultJwsHeader) {
throw new JsonProcessingException('simulate json processing exception on claims') return super.toJson(o)
}
throw new SerializationException('dummy text', new Exception())
} }
} }
@ -211,7 +212,7 @@ class DefaultJwtBuilderTest {
b.setClaims(c).compressWith(CompressionCodecs.DEFLATE).compact() b.setClaims(c).compressWith(CompressionCodecs.DEFLATE).compact()
fail() fail()
} catch (IllegalArgumentException iae) { } 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 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 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.Decoder
import io.jsonwebtoken.codec.DecodingException 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 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 // 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 // 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) @Test(expected = IllegalArgumentException)
void testBase64UrlDecodeWithNullArgument() { void testBase64UrlDecodeWithNullArgument() {
new DefaultJwtBuilder().base64UrlEncodeWith(null) new DefaultJwtParser().base64UrlDecodeWith(null)
} }
@Test @Test
void testBase64UrlEncodeWithCustomEncoder() { void testBase64UrlEncodeWithCustomDecoder() {
def decoder = new Decoder() { def decoder = new Decoder() {
@Override @Override
Object decode(Object o) throws DecodingException { Object decode(Object o) throws DecodingException {
@ -27,4 +39,76 @@ class DefaultJwtParserTest {
def b = new DefaultJwtParser().base64UrlDecodeWith(decoder) def b = new DefaultJwtParser().base64UrlDecodeWith(decoder)
assertSame decoder, b.base64UrlDecoder 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 package io.jsonwebtoken.impl
import io.jsonwebtoken.lang.DateFormats
import org.junit.Test import org.junit.Test
import static org.junit.Assert.* import static org.junit.Assert.*
class JwtMapTest { class JwtMapTest {
@ -28,26 +30,98 @@ class JwtMapTest {
@Test @Test
void testToDateFromDate() { void testToDateFromDate() {
def d = new Date() def d = new Date()
Date date = JwtMap.toDate(d, 'foo') Date date = JwtMap.toDate(d, 'foo')
assertSame date, d assertSame date, d
} }
@Test @Test
void testToDateFromString() { void testToDateFromCalendar() {
def c = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
Date d = new Date(2015, 1, 1, 12, 0, 0) def d = c.getTime()
Date date = JwtMap.toDate(c, 'foo')
String s = (d.getTime() / 1000) + '' //JWT timestamps are in seconds - need to strip millis
Date date = JwtMap.toDate(s, 'foo')
assertEquals date, d 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 @Test
@ -56,7 +130,7 @@ class JwtMapTest {
JwtMap.toDate(new Object() { @Override public String toString() {return 'hi'} }, 'foo') JwtMap.toDate(new Object() { @Override public String toString() {return 'hi'} }, 'foo')
fail() fail()
} catch (IllegalStateException iae) { } 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 @Test
public void testEquals() throws Exception { void testEquals() throws Exception {
def m1 = new JwtMap(); def m1 = new JwtMap();
m1.put("a", "a"); m1.put("a", "a");
@ -137,7 +211,7 @@ class JwtMapTest {
} }
@Test @Test
public void testHashcode() throws Exception { void testHashcode() throws Exception {
def m = new JwtMap(); def m = new JwtMap();
def hashCodeEmpty = m.hashCode(); 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: '世界']])
}
}