Skip to content

Commit

Permalink
Merge pull request #344 from jwtk/issue-335-custom-json
Browse files Browse the repository at this point in the history
Pluggable JSON serialization
  • Loading branch information
lhazlewood authored Jul 11, 2018
2 parents bae78f0 + 8afca0d commit 2917260
Show file tree
Hide file tree
Showing 33 changed files with 1,790 additions and 206 deletions.
27 changes: 18 additions & 9 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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>
Expand Down Expand Up @@ -295,23 +305,22 @@
<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>
<id>clover</id>
<phase>test</phase>
<goals>
<goal>instrument</goal>
<goal>check</goal>
<goal>clover</goal>
<goal>check</goal>
</goals>
</execution>
</executions>
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/io/jsonwebtoken/JwtBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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>
Expand Down
30 changes: 24 additions & 6 deletions src/main/java/io/jsonwebtoken/JwtParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@

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.
Expand Down Expand Up @@ -198,7 +200,7 @@ public interface JwtParser {
* {@code byte[]} variant will be removed before the 1.0.0 release.</p>
*
* @param base64EncodedSecretKey the BASE64-encoded algorithm-specific signature verification key to use to validate
* any discovered JWS digital signature.
* any discovered JWS digital signature.
* @return the parser for method chaining.
*/
JwtParser setSigningKey(String base64EncodedSecretKey);
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -369,7 +387,7 @@ public interface JwtParser {
* @since 0.2
*/
<T> T parse(String jwt, JwtHandler<T> handler)
throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException;
throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException;

/**
* Parses the specified compact serialized JWT string based on the builder's current configuration state and
Expand Down Expand Up @@ -399,7 +417,7 @@ <T> T parse(String jwt, JwtHandler<T> handler)
* @since 0.2
*/
Jwt<Header, String> parsePlaintextJwt(String plaintextJwt)
throws UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException;
throws UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException;

/**
* Parses the specified compact serialized JWT string based on the builder's current configuration state and
Expand Down Expand Up @@ -430,7 +448,7 @@ Jwt<Header, String> parsePlaintextJwt(String plaintextJwt)
* @since 0.2
*/
Jwt<Header, Claims> parseClaimsJwt(String claimsJwt)
throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException;
throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException;

/**
* Parses the specified compact serialized JWS string based on the builder's current configuration state and
Expand Down Expand Up @@ -458,7 +476,7 @@ Jwt<Header, Claims> parseClaimsJwt(String claimsJwt)
* @since 0.2
*/
Jws<String> parsePlaintextJws(String plaintextJws)
throws UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException;
throws UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException;

/**
* Parses the specified compact serialized JWS string based on the builder's current configuration state and
Expand Down Expand Up @@ -487,5 +505,5 @@ Jws<String> parsePlaintextJws(String plaintextJws)
* @since 0.2
*/
Jws<Claims> parseClaimsJws(String claimsJws)
throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException;
throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException;
}
34 changes: 22 additions & 12 deletions src/main/java/io/jsonwebtoken/impl/DefaultClaims.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public Date getExpiration() {

@Override
public Claims setExpiration(Date exp) {
setDate(Claims.EXPIRATION, exp);
setDateAsSeconds(Claims.EXPIRATION, exp);
return this;
}

Expand All @@ -82,7 +82,7 @@ public Date getNotBefore() {

@Override
public Claims setNotBefore(Date nbf) {
setDate(Claims.NOT_BEFORE, nbf);
setDateAsSeconds(Claims.NOT_BEFORE, nbf);
return this;
}

Expand All @@ -93,7 +93,7 @@ public Date getIssuedAt() {

@Override
public Claims setIssuedAt(Date iat) {
setDate(Claims.ISSUED_AT, iat);
setDateAsSeconds(Claims.ISSUED_AT, iat);
return this;
}

Expand All @@ -108,25 +108,35 @@ public Claims setId(String jti) {
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 (value == null) {
return null;
}

if (Claims.EXPIRATION.equals(claimName) ||
Claims.ISSUED_AT.equals(claimName) ||
Claims.NOT_BEFORE.equals(claimName)
) {
value = getDate(claimName);
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;
Expand Down
44 changes: 34 additions & 10 deletions src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,7 +27,11 @@
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;

Expand All @@ -40,19 +42,26 @@

public class DefaultJwtBuilder implements JwtBuilder {

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

private Header header;
private Claims claims;
private String payload;

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.");
Expand Down Expand Up @@ -270,6 +279,14 @@ public JwtBuilder claim(String name, Object value) {

@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.");
}
Expand Down Expand Up @@ -304,8 +321,8 @@ public String compact() {
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) {
Expand Down Expand Up @@ -339,18 +356,25 @@ protected JwtSigner createSigner(SignatureAlgorithm alg, Key key) {
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);
}
}
Loading

0 comments on commit 2917260

Please sign in to comment.