From 19f6fcaa51d9f64cfb20c136b002f7ef51c2b0a4 Mon Sep 17 00:00:00 2001 From: josebarrueta Date: Wed, 23 Sep 2015 13:21:08 -0700 Subject: [PATCH 1/4] Issue-52 Adding ability to compress/decompress. Added tests for happy path. --- .gitignore | 1 + .../io/jsonwebtoken/CompressionCodec.java | 30 +++++++ .../CompressionCodecResolver.java | 27 ++++++ .../io/jsonwebtoken/CompressionException.java | 33 +++++++ src/main/java/io/jsonwebtoken/Header.java | 7 ++ src/main/java/io/jsonwebtoken/JwtBuilder.java | 9 ++ src/main/java/io/jsonwebtoken/JwtParser.java | 8 ++ .../io/jsonwebtoken/impl/DefaultHeader.java | 12 +++ .../jsonwebtoken/impl/DefaultJwtBuilder.java | 46 ++++++++-- .../jsonwebtoken/impl/DefaultJwtParser.java | 26 +++++- .../impl/compression/CompressionCodecs.java | 34 +++++++ .../DefaultCompressionCodecResolver.java | 52 +++++++++++ .../compression/DeflateCompressionCodec.java | 84 ++++++++++++++++++ .../compression/GzipCompressionCodec.java | 88 +++++++++++++++++++ .../java/io/jsonwebtoken/lang/Objects.java | 17 ++++ .../java/io/jsonwebtoken/lang/Strings.java | 3 + .../CompressionExceptionTest.groovy | 41 +++++++++ .../groovy/io/jsonwebtoken/JwtsTest.groovy | 72 +++++++++++++++ .../impl/DefaultJwtBuilderTest.groovy | 2 +- 19 files changed, 581 insertions(+), 11 deletions(-) create mode 100644 src/main/java/io/jsonwebtoken/CompressionCodec.java create mode 100644 src/main/java/io/jsonwebtoken/CompressionCodecResolver.java create mode 100644 src/main/java/io/jsonwebtoken/CompressionException.java create mode 100644 src/main/java/io/jsonwebtoken/impl/compression/CompressionCodecs.java create mode 100644 src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java create mode 100644 src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java create mode 100644 src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java create mode 100644 src/test/groovy/io/jsonwebtoken/CompressionExceptionTest.groovy diff --git a/.gitignore b/.gitignore index a14e67cf..611af4c7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.class +.DS_Store # Mobile Tools for Java (J2ME) .mtj.tmp/ diff --git a/src/main/java/io/jsonwebtoken/CompressionCodec.java b/src/main/java/io/jsonwebtoken/CompressionCodec.java new file mode 100644 index 00000000..e6a24481 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/CompressionCodec.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2015 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken; + +/** + * CompressionCodec + * + * @since 0.59 + */ +public interface CompressionCodec { + + String getAlgorithmName(); + + byte[] compress(byte[] payload); + + byte[] decompress(byte[] compressed); +} \ No newline at end of file diff --git a/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java b/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java new file mode 100644 index 00000000..76805e06 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2015 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken; + +/** + * CompressionCodecResolver + * + * @since 0.5.2 + */ +public interface CompressionCodecResolver { + + CompressionCodec resolveCompressionCodec(Header header); + +} diff --git a/src/main/java/io/jsonwebtoken/CompressionException.java b/src/main/java/io/jsonwebtoken/CompressionException.java new file mode 100644 index 00000000..d4ac62d3 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/CompressionException.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2015 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken; + +/** + * Exception indicating that either compressing or decompressing an JWT body failed. + * + * @since 0.5.2 + */ +public class CompressionException extends JwtException { + + public CompressionException(String message) { + super(message); + } + + public CompressionException(String message, Throwable cause) { + super(message, cause); + } + +} \ No newline at end of file diff --git a/src/main/java/io/jsonwebtoken/Header.java b/src/main/java/io/jsonwebtoken/Header.java index 00893a58..f4b5cfbf 100644 --- a/src/main/java/io/jsonwebtoken/Header.java +++ b/src/main/java/io/jsonwebtoken/Header.java @@ -48,6 +48,9 @@ public interface Header> extends Map { /** JWT {@code Content Type} header parameter name: "cty" */ public static final String CONTENT_TYPE = "cty"; + /** JWT {@code Compression Algorithm} header parameter name: "calg" */ + public static final String COMPRESSION_ALGORITHM = "calg"; + /** * Returns the * typ (type) header value or {@code null} if not present. @@ -100,4 +103,8 @@ public interface Header> extends Map { */ T setContentType(String cty); + String getCompressionAlgorithm(); + + T setCompressionAlgorithm(String compressionAlgorithm); + } diff --git a/src/main/java/io/jsonwebtoken/JwtBuilder.java b/src/main/java/io/jsonwebtoken/JwtBuilder.java index cb5339b5..10ae9682 100644 --- a/src/main/java/io/jsonwebtoken/JwtBuilder.java +++ b/src/main/java/io/jsonwebtoken/JwtBuilder.java @@ -349,6 +349,15 @@ public interface JwtBuilder extends ClaimsMutator { */ JwtBuilder signWith(SignatureAlgorithm alg, Key key); + /** + * Compresses the JWT body before being signed. + * + * @param codec implementation of the {@link CompressionCodec} to be used. + * @return the builder for method chaining. + * @since 0.5.2 + */ + JwtBuilder compressWith(CompressionCodec codec); + /** * Actually builds the JWT and serializes it to a compact, URL-safe string according to the * JWT Compact Serialization diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index f4b190ba..909e73d0 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -107,6 +107,14 @@ public interface JwtParser { */ JwtParser setSigningKeyResolver(SigningKeyResolver signingKeyResolver); + /** + * + * @param compressionCodecResolver + * @return the parser for method chaining. + * @since 0.5.2 + */ + JwtParser setCompressionCodecResolver(CompressionCodecResolver compressionCodecResolver); + /** * Returns {@code true} if the specified JWT compact string represents a signed JWT (aka a 'JWS'), {@code false} * otherwise. diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java b/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java index 52ffc8b9..74efdf9d 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java @@ -51,4 +51,16 @@ public class DefaultHeader> extends JwtMap implements Header setValue(CONTENT_TYPE, cty); return (T)this; } + + @Override + public String getCompressionAlgorithm() { + return getString(COMPRESSION_ALGORITHM); + } + + @Override + public T setCompressionAlgorithm(String compressionAlgorithm) { + setValue(COMPRESSION_ALGORITHM, compressionAlgorithm); + return (T) this; + } + } diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java index 6509ab00..76d3a2c3 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java @@ -18,6 +18,7 @@ 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; import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.JwtBuilder; @@ -48,6 +49,8 @@ public class DefaultJwtBuilder implements JwtBuilder { private Key key; private byte[] keyBytes; + private CompressionCodec compressionCodec; + @Override public JwtBuilder setHeader(Header header) { this.header = header; @@ -113,6 +116,13 @@ public class DefaultJwtBuilder implements JwtBuilder { return this; } + @Override + public JwtBuilder compressWith(CompressionCodec compressionCodec) { + Assert.notNull(compressionCodec, "compressionCodec cannot be null"); + this.compressionCodec = compressionCodec; + return this; + } + @Override public JwtBuilder setPayload(String payload) { this.payload = payload; @@ -279,11 +289,30 @@ public class DefaultJwtBuilder implements JwtBuilder { jwsHeader.setAlgorithm(SignatureAlgorithm.NONE.getValue()); } + if (compressionCodec != null) { + jwsHeader.setCompressionAlgorithm(compressionCodec.getAlgorithmName()); + } + String base64UrlEncodedHeader = base64UrlEncode(jwsHeader, "Unable to serialize header to json."); - String base64UrlEncodedBody = this.payload != null ? - TextCodec.BASE64URL.encode(this.payload) : - base64UrlEncode(claims, "Unable to serialize claims object to json."); + String base64UrlEncodedBody; + + if (compressionCodec != null) { + + 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."); + } + + base64UrlEncodedBody = TextCodec.BASE64URL.encode(compressionCodec.compress(bytes)); + + } else { + base64UrlEncodedBody = this.payload != null ? + TextCodec.BASE64URL.encode(this.payload) : + base64UrlEncode(claims, "Unable to serialize claims object to json."); + } String jwt = base64UrlEncodedHeader + JwtParser.SEPARATOR_CHAR + base64UrlEncodedBody; @@ -311,17 +340,18 @@ public class DefaultJwtBuilder implements JwtBuilder { } protected String base64UrlEncode(Object o, String errMsg) { - String s; + byte[] bytes; try { - s = toJson(o); + bytes = toJson(o); } catch (JsonProcessingException e) { throw new IllegalStateException(errMsg, e); } - return TextCodec.BASE64URL.encode(s); + return TextCodec.BASE64URL.encode(bytes); } - protected String toJson(Object o) throws JsonProcessingException { - return OBJECT_MAPPER.writeValueAsString(o); + + protected byte[] toJson(Object object) throws JsonProcessingException { + return OBJECT_MAPPER.writeValueAsBytes(object); } } diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index ba888e04..a3b372b2 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -17,11 +17,12 @@ package io.jsonwebtoken.impl; import com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.Claims; +import io.jsonwebtoken.CompressionCodec; +import io.jsonwebtoken.CompressionCodecResolver; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Header; import io.jsonwebtoken.Jws; import io.jsonwebtoken.JwsHeader; -import io.jsonwebtoken.SigningKeyResolver; import io.jsonwebtoken.Jwt; import io.jsonwebtoken.JwtHandler; import io.jsonwebtoken.JwtHandlerAdapter; @@ -30,7 +31,9 @@ import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.PrematureJwtException; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.SignatureException; +import io.jsonwebtoken.SigningKeyResolver; import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver; import io.jsonwebtoken.impl.crypto.DefaultJwtSignatureValidator; import io.jsonwebtoken.impl.crypto.JwtSignatureValidator; import io.jsonwebtoken.lang.Assert; @@ -58,6 +61,8 @@ public class DefaultJwtParser implements JwtParser { private SigningKeyResolver signingKeyResolver; + private CompressionCodecResolver compressionCodecResolver = new DefaultCompressionCodecResolver(); + @Override public JwtParser setSigningKey(byte[] key) { Assert.notEmpty(key, "signing key cannot be null or empty."); @@ -86,6 +91,13 @@ public class DefaultJwtParser implements JwtParser { return this; } + @Override + public JwtParser setCompressionCodecResolver(CompressionCodecResolver compressionCodecResolver) { + Assert.notNull(compressionCodecResolver, "compressionCodecResolver cannot be null."); + this.compressionCodecResolver = compressionCodecResolver; + return this; + } + @Override public boolean isSigned(String jwt) { @@ -157,6 +169,8 @@ public class DefaultJwtParser implements JwtParser { // =============== Header ================= Header header = null; + CompressionCodec compressionCodec = null; + if (base64UrlEncodedHeader != null) { String origValue = TextCodec.BASE64URL.decodeToString(base64UrlEncodedHeader); Map m = readValue(origValue); @@ -166,10 +180,18 @@ public class DefaultJwtParser implements JwtParser { } else { header = new DefaultHeader(m); } + + compressionCodec = compressionCodecResolver.resolveCompressionCodec(header); } // =============== Body ================= - String payload = TextCodec.BASE64URL.decodeToString(base64UrlEncodedPayload); + String payload; + if (compressionCodec != null) { + byte[] decompressed = compressionCodec.decompress(TextCodec.BASE64URL.decode(base64UrlEncodedPayload)); + payload = new String(decompressed, Strings.UTF_8); + } else { + payload = TextCodec.BASE64URL.decodeToString(base64UrlEncodedPayload); + } Claims claims = null; diff --git a/src/main/java/io/jsonwebtoken/impl/compression/CompressionCodecs.java b/src/main/java/io/jsonwebtoken/impl/compression/CompressionCodecs.java new file mode 100644 index 00000000..d1d663d3 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/compression/CompressionCodecs.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2015 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.compression; + +import io.jsonwebtoken.CompressionCodec; + +/** + * CompressionCodecs exposes default implementation of the {@link CompressionCodec} interface. + * + * @since 0.5.2 + */ +public abstract class CompressionCodecs { + + private CompressionCodecs(){} + + public static final CompressionCodec DEFLATE = new DeflateCompressionCodec(); + + public static final CompressionCodec GZIP = new GzipCompressionCodec(); + + +} diff --git a/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java b/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java new file mode 100644 index 00000000..00b37a88 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2015 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.compression; + +import io.jsonwebtoken.CompressionCodec; +import io.jsonwebtoken.CompressionCodecResolver; +import io.jsonwebtoken.CompressionException; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Strings; + +/** + * DefaultCompressionCodecResolver + * + * @since 0.5.2 + */ +public class DefaultCompressionCodecResolver implements CompressionCodecResolver { + + @Override + public CompressionCodec resolveCompressionCodec(Header header) { + Assert.notNull(header, "header cannot be null."); + + String cmpAlg = header.getCompressionAlgorithm(); + + if (!Strings.hasText(cmpAlg)) { + return null; + } + + if (CompressionCodecs.DEFLATE.getAlgorithmName().equalsIgnoreCase(cmpAlg)) { + return CompressionCodecs.DEFLATE; + } + + if (CompressionCodecs.GZIP.getAlgorithmName().equalsIgnoreCase(cmpAlg)) { + return CompressionCodecs.GZIP; + } + + throw new CompressionException("Unsupported compression algorithm '" + cmpAlg + "'"); + } +} diff --git a/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java b/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java new file mode 100644 index 00000000..83b5725d --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2015 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.compression; + +import io.jsonwebtoken.CompressionCodec; +import io.jsonwebtoken.CompressionException; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Objects; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.InflaterOutputStream; + +/** + * DeflateCompressionCodec + * + * @since 0.5.2 + */ +public class DeflateCompressionCodec implements CompressionCodec { + + private static final String DEFLATE = "DEF"; + + @Override + public String getAlgorithmName() { + return DEFLATE; + } + + @Override + public byte[] compress(byte[] payload) { + Assert.notNull(payload, "payload cannot be null."); + + Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION); + + ByteArrayOutputStream outputStream = null; + DeflaterOutputStream deflaterOutputStream = null; + try { + outputStream = new ByteArrayOutputStream(); + deflaterOutputStream = new DeflaterOutputStream(outputStream, deflater, true); + + deflaterOutputStream.write(payload, 0, payload.length); + deflaterOutputStream.flush(); + return outputStream.toByteArray(); + } catch (IOException e) { + throw new CompressionException("Unable to compress payload.", e); + } finally { + Objects.nullSafeClose(outputStream, deflaterOutputStream); + } + } + + @Override + public byte[] decompress(byte[] compressed) { + Assert.notNull(compressed, "compressed cannot be null."); + + InflaterOutputStream inflaterOutputStream = null; + ByteArrayOutputStream decompressedOutputStream = null; + + try { + decompressedOutputStream = new ByteArrayOutputStream(); + inflaterOutputStream = new InflaterOutputStream(decompressedOutputStream); + inflaterOutputStream.write(compressed); + inflaterOutputStream.flush(); + return decompressedOutputStream.toByteArray(); + } catch (IOException e) { + throw new CompressionException("Unable to decompress compressed payload.", e); + } finally { + Objects.nullSafeClose(decompressedOutputStream, inflaterOutputStream); + } + } +} diff --git a/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java b/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java new file mode 100644 index 00000000..1a8a7a53 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2015 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.compression; + +import io.jsonwebtoken.CompressionCodec; +import io.jsonwebtoken.CompressionException; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Objects; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +/** + * GzipCompressionCodec + * + * @since 0.5.2 + */ +public class GzipCompressionCodec implements CompressionCodec { + + private static final String GZIP = "GZIP"; + + @Override + public String getAlgorithmName() { + return GZIP; + } + + @Override + public byte[] compress(byte[] payload) { + Assert.notNull(payload, "payload cannot be null."); + + ByteArrayOutputStream outputStream = null; + GZIPOutputStream gzipOutputStream = null; + + try { + outputStream = new ByteArrayOutputStream(); + gzipOutputStream = new GZIPOutputStream(outputStream, true); + gzipOutputStream.write(payload, 0, payload.length); + gzipOutputStream.finish(); + return outputStream.toByteArray(); + } catch (IOException e) { + throw new CompressionException("Unable to compress payload.", e); + } finally { + Objects.nullSafeClose(outputStream, gzipOutputStream); + } + } + + @Override + public byte[] decompress(byte[] compressed) { + Assert.notNull(compressed, "compressed cannot be null."); + + byte[] buffer = new byte[512]; + + ByteArrayOutputStream outputStream = null; + GZIPInputStream gzipInputStream = null; + ByteArrayInputStream inputStream = null; + + try { + inputStream = new ByteArrayInputStream(compressed); + gzipInputStream = new GZIPInputStream(inputStream); + outputStream = new ByteArrayOutputStream(); + int read; + while ((read = gzipInputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, read); + } + return outputStream.toByteArray(); + } catch (IOException e) { + throw new CompressionException("Unable to decompress compressed payload.", e); + } finally { + Objects.nullSafeClose(inputStream, gzipInputStream, outputStream); + } + } +} diff --git a/src/main/java/io/jsonwebtoken/lang/Objects.java b/src/main/java/io/jsonwebtoken/lang/Objects.java index dbdc84ce..acc4b288 100644 --- a/src/main/java/io/jsonwebtoken/lang/Objects.java +++ b/src/main/java/io/jsonwebtoken/lang/Objects.java @@ -15,6 +15,8 @@ */ package io.jsonwebtoken.lang; +import java.io.Closeable; +import java.io.IOException; import java.lang.reflect.Array; import java.util.Arrays; @@ -905,4 +907,19 @@ public abstract class Objects { return sb.toString(); } + public static void nullSafeClose(Closeable... closeables) { + if (closeables == null) { + return; + } + + for (Closeable closeable : closeables) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException e) { + //Ignore the exception during close. + } + } + } + } } diff --git a/src/main/java/io/jsonwebtoken/lang/Strings.java b/src/main/java/io/jsonwebtoken/lang/Strings.java index 3fa1b6ca..8e1157f4 100644 --- a/src/main/java/io/jsonwebtoken/lang/Strings.java +++ b/src/main/java/io/jsonwebtoken/lang/Strings.java @@ -15,6 +15,7 @@ */ package io.jsonwebtoken.lang; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -40,6 +41,8 @@ public abstract class Strings { private static final char EXTENSION_SEPARATOR = '.'; + public static final Charset UTF_8 = Charset.forName("UTF-8"); + //--------------------------------------------------------------------- // General convenience methods for working with Strings //--------------------------------------------------------------------- diff --git a/src/test/groovy/io/jsonwebtoken/CompressionExceptionTest.groovy b/src/test/groovy/io/jsonwebtoken/CompressionExceptionTest.groovy new file mode 100644 index 00000000..f588ebbd --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/CompressionExceptionTest.groovy @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2015 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken + +import org.junit.Test + +import static org.junit.Assert.assertEquals + +class CompressionExceptionTest { + + @Test + void testDefaultConstructor() { + def exception = new CompressionException("my message") + + assertEquals "my message", exception.getMessage() + } + + @Test + void testConstructorWithCause() { + + def ioException = new IOException("root error") + + def exception = new CompressionException("wrapping", ioException) + + assertEquals "wrapping", exception.getMessage() + assertEquals ioException, exception.getCause() + } +} \ No newline at end of file diff --git a/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index b8e6306a..c20ceda4 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import io.jsonwebtoken.impl.DefaultHeader import io.jsonwebtoken.impl.DefaultJwsHeader import io.jsonwebtoken.impl.TextCodec +import io.jsonwebtoken.impl.compression.CompressionCodecs import io.jsonwebtoken.impl.crypto.EllipticCurveProvider import io.jsonwebtoken.impl.crypto.MacProvider import io.jsonwebtoken.impl.crypto.RsaProvider @@ -186,6 +187,16 @@ class JwtsTest { } } + @Test + void testWithInvalidCompressionAlgorithm() { + try { + + Jwts.builder().setHeaderParam(Header.COMPRESSION_ALGORITHM, "CUSTOM").setId("andId").compact() + } catch (CompressionException e) { + assertEquals "Unsupported compression algorithm 'CUSTOM'", e.getMessage() + } + } + @Test void testConvenienceIssuer() { String compact = Jwts.builder().setIssuer("Me").compact(); @@ -320,6 +331,67 @@ class JwtsTest { assertNull claims.getId() } + @Test + void testCompressedJwtWithDeflate() { + + byte[] key = MacProvider.generateKey().getEncoded() + + String id = UUID.randomUUID().toString() + + String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(SignatureAlgorithm.HS256, key) + .claim("state", "hello this is an amazing jwt").compressWith(CompressionCodecs.DEFLATE).compact() + + def jws = Jwts.parser().setSigningKey(key).parseClaimsJws(compact) + + Claims claims = jws.body + + assertEquals "DEF", jws.header.getCompressionAlgorithm() + + assertEquals id, claims.getId() + assertEquals "an audience", claims.getAudience() + assertEquals "hello this is an amazing jwt", claims.state + } + + @Test + void testCompressedJwtWithGZIP() { + + byte[] key = MacProvider.generateKey().getEncoded() + + String id = UUID.randomUUID().toString() + + String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(SignatureAlgorithm.HS256, key) + .claim("state", "hello this is an amazing jwt").compressWith(CompressionCodecs.GZIP).compact() + + def jws = Jwts.parser().setSigningKey(key).parseClaimsJws(compact) + + Claims claims = jws.body + + assertEquals "GZIP", jws.header.getCompressionAlgorithm() + + assertEquals id, claims.getId() + assertEquals "an audience", claims.getAudience() + assertEquals "hello this is an amazing jwt", claims.state + } + + @Test + void testCompressStringPayloadWithDeflate() { + + byte[] key = MacProvider.generateKey().getEncoded() + + String payload = "this is my test for a payload" + + String compact = Jwts.builder().setPayload(payload).signWith(SignatureAlgorithm.HS256, key) + .compressWith(CompressionCodecs.DEFLATE).compact() + + def jws = Jwts.parser().setSigningKey(key).parsePlaintextJws(compact) + + String parsed = jws.body + + assertEquals "DEF", jws.header.getCompressionAlgorithm() + + assertEquals "this is my test for a payload", parsed + } + @Test void testHS256() { testHmac(SignatureAlgorithm.HS256); diff --git a/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy index 7090d03c..4ee7d8e5 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy @@ -173,7 +173,7 @@ class DefaultJwtBuilderTest { def b = new DefaultJwtBuilder() { @Override - protected String toJson(Object o) throws JsonProcessingException { + protected byte[] toJson(Object o) throws JsonProcessingException { throw new JsonMappingException('foo') } } From 806844a89a279f8e57fbee9121a03695cf68215b Mon Sep 17 00:00:00 2001 From: Jason Erickson Date: Wed, 23 Sep 2015 15:44:07 -0700 Subject: [PATCH 2/4] Issue-52: Refactoring and adding unit tests to cover the compression functionality --- .../io/jsonwebtoken/CompressionCodec.java | 18 ++++- .../CompressionCodecResolver.java | 8 +- .../compression/BaseCompressionCodec.java | 79 ++++++++++++++++++ .../impl/compression/CompressionCodecs.java | 15 ++-- .../DefaultCompressionCodecResolver.java | 18 +++-- .../compression/DeflateCompressionCodec.java | 19 +---- .../compression/GzipCompressionCodec.java | 44 ++++------ .../groovy/io/jsonwebtoken/JwtsTest.groovy | 80 +++++++++++++++++++ .../BaseCompressionCodecTest.groovy | 55 +++++++++++++ 9 files changed, 275 insertions(+), 61 deletions(-) create mode 100644 src/main/java/io/jsonwebtoken/impl/compression/BaseCompressionCodec.java create mode 100644 src/test/groovy/io/jsonwebtoken/impl/compression/BaseCompressionCodecTest.groovy diff --git a/src/main/java/io/jsonwebtoken/CompressionCodec.java b/src/main/java/io/jsonwebtoken/CompressionCodec.java index e6a24481..84c56a29 100644 --- a/src/main/java/io/jsonwebtoken/CompressionCodec.java +++ b/src/main/java/io/jsonwebtoken/CompressionCodec.java @@ -16,15 +16,29 @@ package io.jsonwebtoken; /** - * CompressionCodec + * Defines how to compress and decompress byte arrays. * - * @since 0.59 + * @since 0.5.2 */ public interface CompressionCodec { + /** + * The algorithm name that would appear in the JWT header. + * @return the algorithm name that would appear in the JWT header + */ String getAlgorithmName(); + /** + * Takes a byte array and returns a compressed version. + * @param payload bytes to compress + * @return compressed bytes + */ byte[] compress(byte[] payload); + /** + * Takes a compressed byte array and returns a decompressed version. + * @param compressed compressed bytes + * @return decompressed bytes + */ byte[] decompress(byte[] compressed); } \ No newline at end of file diff --git a/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java b/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java index 76805e06..877ab890 100644 --- a/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java +++ b/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java @@ -16,12 +16,16 @@ package io.jsonwebtoken; /** - * CompressionCodecResolver + * Resolves "calg" header to an implementation of CompressionCodec. * * @since 0.5.2 */ public interface CompressionCodecResolver { - + /** + * Examines the header and returns a CompressionCodec if it finds one that it recognizes. + * @param header of the JWT + * @return CompressionCodec matching the "calg" header, or null if there is no "calg" header. + */ CompressionCodec resolveCompressionCodec(Header header); } diff --git a/src/main/java/io/jsonwebtoken/impl/compression/BaseCompressionCodec.java b/src/main/java/io/jsonwebtoken/impl/compression/BaseCompressionCodec.java new file mode 100644 index 00000000..1c86613a --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/compression/BaseCompressionCodec.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2015 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.compression; + +import io.jsonwebtoken.CompressionCodec; +import io.jsonwebtoken.CompressionException; +import io.jsonwebtoken.lang.Assert; + +import java.io.IOException; + +/** + * Base class that asserts arguments and wraps IOException with CompressionException. + * + * @since 0.5.2 + */ +public abstract class BaseCompressionCodec implements CompressionCodec { + /** + * Implement this method to do the actual work of compressing the payload + * @param payload the bytes to compress + * @return the compressed bytes + * @throws IOException if the compression causes an IOException + */ + protected abstract byte[] doCompress(byte[] payload) throws IOException; + + /** + * Asserts that payload is not null and calls doCompress + * @param payload bytes to compress + * @return compressed bytes + * @throws CompressionException if doCompress throws an IOException + */ + @Override + public final byte[] compress(byte[] payload) { + Assert.notNull(payload, "payload cannot be null."); + + try { + return doCompress(payload); + } catch (IOException e) { + throw new CompressionException("Unable to compress payload.", e); + } + } + + /** + * Asserts the compressed bytes is not null and calls doDecompress + * @param compressed compressed bytes + * @return decompressed bytes + * @throws CompressionException if doCompress throws an IOException + */ + @Override + public final byte[] decompress(byte[] compressed) { + Assert.notNull(compressed, "compressed bytes cannot be null."); + + try { + return doDecompress(compressed); + } catch (IOException e) { + throw new CompressionException("Unable to decompress bytes.", e); + } + } + + /** + * Implement this method to do the actual work of decompressing the compressed bytes. + * @param compressed compressed bytes + * @return decompressed bytes + * @throws IOException if the decompression runs into an IO problem + */ + protected abstract byte[] doDecompress(byte[] compressed) throws IOException; +} diff --git a/src/main/java/io/jsonwebtoken/impl/compression/CompressionCodecs.java b/src/main/java/io/jsonwebtoken/impl/compression/CompressionCodecs.java index d1d663d3..8a1fd7eb 100644 --- a/src/main/java/io/jsonwebtoken/impl/compression/CompressionCodecs.java +++ b/src/main/java/io/jsonwebtoken/impl/compression/CompressionCodecs.java @@ -22,13 +22,16 @@ import io.jsonwebtoken.CompressionCodec; * * @since 0.5.2 */ -public abstract class CompressionCodecs { +public interface CompressionCodecs { - private CompressionCodecs(){} - - public static final CompressionCodec DEFLATE = new DeflateCompressionCodec(); - - public static final CompressionCodec GZIP = new GzipCompressionCodec(); + /** + * Codec implementing the deflate compression algorithm + */ + CompressionCodec DEFLATE = new DeflateCompressionCodec(); + /** + * Codec implementing the gzip compression algorithm + */ + CompressionCodec GZIP = new GzipCompressionCodec(); } diff --git a/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java b/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java index 00b37a88..298c3b57 100644 --- a/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java +++ b/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java @@ -23,7 +23,8 @@ import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Strings; /** - * DefaultCompressionCodecResolver + * Default implementation of {@link CompressionCodecResolver}. This implementation will resolve DEF to + * {@link DeflateCompressionCodec} and GZIP to {@link GzipCompressionCodec}. * * @since 0.5.2 */ @@ -31,22 +32,25 @@ public class DefaultCompressionCodecResolver implements CompressionCodecResolver @Override public CompressionCodec resolveCompressionCodec(Header header) { - Assert.notNull(header, "header cannot be null."); + String cmpAlg = getAlgorithmFromHeader(header); - String cmpAlg = header.getCompressionAlgorithm(); - - if (!Strings.hasText(cmpAlg)) { + final boolean hasCompressionAlgorithm = Strings.hasText(cmpAlg); + if (!hasCompressionAlgorithm) { return null; } - if (CompressionCodecs.DEFLATE.getAlgorithmName().equalsIgnoreCase(cmpAlg)) { return CompressionCodecs.DEFLATE; } - if (CompressionCodecs.GZIP.getAlgorithmName().equalsIgnoreCase(cmpAlg)) { return CompressionCodecs.GZIP; } throw new CompressionException("Unsupported compression algorithm '" + cmpAlg + "'"); } + + protected final String getAlgorithmFromHeader(Header header) { + Assert.notNull(header, "header cannot be null."); + + return header.getCompressionAlgorithm(); + } } diff --git a/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java b/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java index 83b5725d..e7390fbd 100644 --- a/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java +++ b/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java @@ -15,9 +15,6 @@ */ package io.jsonwebtoken.impl.compression; -import io.jsonwebtoken.CompressionCodec; -import io.jsonwebtoken.CompressionException; -import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Objects; import java.io.ByteArrayOutputStream; @@ -27,11 +24,10 @@ import java.util.zip.DeflaterOutputStream; import java.util.zip.InflaterOutputStream; /** - * DeflateCompressionCodec - * + * Codec implementing the deflate compression algorithm * @since 0.5.2 */ -public class DeflateCompressionCodec implements CompressionCodec { +public class DeflateCompressionCodec extends BaseCompressionCodec { private static final String DEFLATE = "DEF"; @@ -41,8 +37,7 @@ public class DeflateCompressionCodec implements CompressionCodec { } @Override - public byte[] compress(byte[] payload) { - Assert.notNull(payload, "payload cannot be null."); + public byte[] doCompress(byte[] payload) throws IOException { Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION); @@ -55,17 +50,13 @@ public class DeflateCompressionCodec implements CompressionCodec { deflaterOutputStream.write(payload, 0, payload.length); deflaterOutputStream.flush(); return outputStream.toByteArray(); - } catch (IOException e) { - throw new CompressionException("Unable to compress payload.", e); } finally { Objects.nullSafeClose(outputStream, deflaterOutputStream); } } @Override - public byte[] decompress(byte[] compressed) { - Assert.notNull(compressed, "compressed cannot be null."); - + public byte[] doDecompress(byte[] compressed) throws IOException { InflaterOutputStream inflaterOutputStream = null; ByteArrayOutputStream decompressedOutputStream = null; @@ -75,8 +66,6 @@ public class DeflateCompressionCodec implements CompressionCodec { inflaterOutputStream.write(compressed); inflaterOutputStream.flush(); return decompressedOutputStream.toByteArray(); - } catch (IOException e) { - throw new CompressionException("Unable to decompress compressed payload.", e); } finally { Objects.nullSafeClose(decompressedOutputStream, inflaterOutputStream); } diff --git a/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java b/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java index 1a8a7a53..61fd98de 100644 --- a/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java +++ b/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java @@ -16,8 +16,6 @@ package io.jsonwebtoken.impl.compression; import io.jsonwebtoken.CompressionCodec; -import io.jsonwebtoken.CompressionException; -import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Objects; import java.io.ByteArrayInputStream; @@ -27,11 +25,11 @@ import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; /** - * GzipCompressionCodec + * Codec implementing the gzip compression algorithm * * @since 0.5.2 */ -public class GzipCompressionCodec implements CompressionCodec { +public class GzipCompressionCodec extends BaseCompressionCodec implements CompressionCodec { private static final String GZIP = "GZIP"; @@ -41,29 +39,7 @@ public class GzipCompressionCodec implements CompressionCodec { } @Override - public byte[] compress(byte[] payload) { - Assert.notNull(payload, "payload cannot be null."); - - ByteArrayOutputStream outputStream = null; - GZIPOutputStream gzipOutputStream = null; - - try { - outputStream = new ByteArrayOutputStream(); - gzipOutputStream = new GZIPOutputStream(outputStream, true); - gzipOutputStream.write(payload, 0, payload.length); - gzipOutputStream.finish(); - return outputStream.toByteArray(); - } catch (IOException e) { - throw new CompressionException("Unable to compress payload.", e); - } finally { - Objects.nullSafeClose(outputStream, gzipOutputStream); - } - } - - @Override - public byte[] decompress(byte[] compressed) { - Assert.notNull(compressed, "compressed cannot be null."); - + protected byte[] doDecompress(byte[] compressed) throws IOException { byte[] buffer = new byte[512]; ByteArrayOutputStream outputStream = null; @@ -79,10 +55,20 @@ public class GzipCompressionCodec implements CompressionCodec { outputStream.write(buffer, 0, read); } return outputStream.toByteArray(); - } catch (IOException e) { - throw new CompressionException("Unable to decompress compressed payload.", e); } finally { Objects.nullSafeClose(inputStream, gzipInputStream, outputStream); } } + + protected byte[] doCompress(byte[] payload) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + GZIPOutputStream compressorOutputStream = new GZIPOutputStream(outputStream, true); + try { + compressorOutputStream.write(payload, 0, payload.length); + compressorOutputStream.finish(); + return outputStream.toByteArray(); + } finally { + Objects.nullSafeClose(compressorOutputStream, outputStream); + } + } } diff --git a/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index c20ceda4..c188ca56 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -20,9 +20,12 @@ import io.jsonwebtoken.impl.DefaultHeader import io.jsonwebtoken.impl.DefaultJwsHeader import io.jsonwebtoken.impl.TextCodec import io.jsonwebtoken.impl.compression.CompressionCodecs +import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver +import io.jsonwebtoken.impl.compression.GzipCompressionCodec import io.jsonwebtoken.impl.crypto.EllipticCurveProvider import io.jsonwebtoken.impl.crypto.MacProvider import io.jsonwebtoken.impl.crypto.RsaProvider +import io.jsonwebtoken.lang.Strings import org.junit.Test import javax.crypto.Mac @@ -109,6 +112,7 @@ class JwtsTest { def token = Jwts.parser().parse(jwt); + //noinspection GrEqualsBetweenInconvertibleTypes assert token.body == claims } @@ -331,6 +335,27 @@ class JwtsTest { assertNull claims.getId() } + @Test + void testUncompressedJwt() { + + byte[] key = MacProvider.generateKey().getEncoded() + + String id = UUID.randomUUID().toString() + + String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(SignatureAlgorithm.HS256, key) + .claim("state", "hello this is an amazing jwt").compact() + + def jws = Jwts.parser().setSigningKey(key).parseClaimsJws(compact) + + Claims claims = jws.body + + assertNull jws.header.getCompressionAlgorithm() + + assertEquals id, claims.getId() + assertEquals "an audience", claims.getAudience() + assertEquals "hello this is an amazing jwt", claims.state + } + @Test void testCompressedJwtWithDeflate() { @@ -373,6 +398,58 @@ class JwtsTest { assertEquals "hello this is an amazing jwt", claims.state } + @Test + void testCompressedWithCustomResolver() { + byte[] key = MacProvider.generateKey().getEncoded() + + String id = UUID.randomUUID().toString() + + String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(SignatureAlgorithm.HS256, key) + .claim("state", "hello this is an amazing jwt").compressWith(new GzipCompressionCodec() { + @Override + String getAlgorithmName() { + return "CUSTOM" + } + }).compact() + + def jws = Jwts.parser().setSigningKey(key).setCompressionCodecResolver(new DefaultCompressionCodecResolver() { + @Override + CompressionCodec resolveCompressionCodec(Header header) { + String algorithm = getAlgorithmFromHeader(header); + if ("CUSTOM".equals(algorithm)) { + return CompressionCodecs.GZIP + } else { + return null + } + } + }).parseClaimsJws(compact) + + Claims claims = jws.body + + assertEquals "CUSTOM", jws.header.getCompressionAlgorithm() + + assertEquals id, claims.getId() + assertEquals "an audience", claims.getAudience() + assertEquals "hello this is an amazing jwt", claims.state + + } + @Test(expected = CompressionException.class) + void testCompressedJwtWithUnrecognizedHeader() { + byte[] key = MacProvider.generateKey().getEncoded() + + String id = UUID.randomUUID().toString() + + String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(SignatureAlgorithm.HS256, key) + .claim("state", "hello this is an amazing jwt").compressWith(new GzipCompressionCodec() { + @Override + String getAlgorithmName() { + return "CUSTOM" + } + }).compact() + + Jwts.parser().setSigningKey(key).parseClaimsJws(compact) + } + @Test void testCompressStringPayloadWithDeflate() { @@ -643,6 +720,7 @@ class JwtsTest { def token = Jwts.parser().setSigningKey(key).parse(jwt); assert [alg: alg.name()] == token.header + //noinspection GrEqualsBetweenInconvertibleTypes assert token.body == claims } @@ -657,6 +735,7 @@ class JwtsTest { def token = Jwts.parser().setSigningKey(key).parse(jwt) assert token.header == [alg: alg.name()] + //noinspection GrEqualsBetweenInconvertibleTypes assert token.body == claims } @@ -678,6 +757,7 @@ class JwtsTest { def token = Jwts.parser().setSigningKey(key).parse(jwt); assert token.header == [alg: alg.name()] + //noinspection GrEqualsBetweenInconvertibleTypes assert token.body == claims } } diff --git a/src/test/groovy/io/jsonwebtoken/impl/compression/BaseCompressionCodecTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/compression/BaseCompressionCodecTest.groovy new file mode 100644 index 00000000..7ffc8eca --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/impl/compression/BaseCompressionCodecTest.groovy @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2015 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.compression + +import io.jsonwebtoken.CompressionCodec +import io.jsonwebtoken.CompressionException +import org.junit.Test + +/** + * @since 0.5.2 + */ +class BaseCompressionCodecTest { + static class ExceptionThrowingCodec extends BaseCompressionCodec { + + @Override + protected byte[] doCompress(byte[] payload) throws IOException { + throw new IOException("Test Exception") + } + + @Override + String getAlgorithmName() { + return "Test" + } + + @Override + protected byte[] doDecompress(byte[] payload) throws IOException { + throw new IOException("Test Decompress Exception"); + } + } + + @Test(expected = CompressionException.class) + void testCompressWithException() { + CompressionCodec codecUT = new ExceptionThrowingCodec(); + codecUT.compress(new byte[0]); + } + + @Test(expected = CompressionException.class) + void testDecompressWithException() { + CompressionCodec codecUT = new ExceptionThrowingCodec(); + codecUT.decompress(new byte[0]); + } +} From 7e15e2de022d7c110c7524cbd7ab9ef5b2c12bab Mon Sep 17 00:00:00 2001 From: Jason Erickson Date: Wed, 23 Sep 2015 17:24:47 -0700 Subject: [PATCH 3/4] Issue-52: Refactoring and adding unit tests to cover the compression functionality --- .../impl/compression/DefaultCompressionCodecResolver.java | 2 +- src/test/groovy/io/jsonwebtoken/JwtsTest.groovy | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java b/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java index 298c3b57..4dde29d5 100644 --- a/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java +++ b/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java @@ -48,7 +48,7 @@ public class DefaultCompressionCodecResolver implements CompressionCodecResolver throw new CompressionException("Unsupported compression algorithm '" + cmpAlg + "'"); } - protected final String getAlgorithmFromHeader(Header header) { + private String getAlgorithmFromHeader(Header header) { Assert.notNull(header, "header cannot be null."); return header.getCompressionAlgorithm(); diff --git a/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index c188ca56..9a344f0b 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -415,7 +415,7 @@ class JwtsTest { def jws = Jwts.parser().setSigningKey(key).setCompressionCodecResolver(new DefaultCompressionCodecResolver() { @Override CompressionCodec resolveCompressionCodec(Header header) { - String algorithm = getAlgorithmFromHeader(header); + String algorithm = header.getCompressionAlgorithm() if ("CUSTOM".equals(algorithm)) { return CompressionCodecs.GZIP } else { From fef553ad72ef0c2aa49ea3f5abddd23f08668657 Mon Sep 17 00:00:00 2001 From: josebarrueta Date: Fri, 9 Oct 2015 18:07:06 -0700 Subject: [PATCH 4/4] Issue-52 Improving Javadoc for compression --- .../io/jsonwebtoken/CompressionCodec.java | 2 ++ src/main/java/io/jsonwebtoken/Header.java | 19 ++++++++++++++++++- src/main/java/io/jsonwebtoken/JwtBuilder.java | 5 ++++- src/main/java/io/jsonwebtoken/JwtParser.java | 4 +++- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/CompressionCodec.java b/src/main/java/io/jsonwebtoken/CompressionCodec.java index 84c56a29..35f03886 100644 --- a/src/main/java/io/jsonwebtoken/CompressionCodec.java +++ b/src/main/java/io/jsonwebtoken/CompressionCodec.java @@ -19,6 +19,8 @@ package io.jsonwebtoken; * Defines how to compress and decompress byte arrays. * * @since 0.5.2 + * @see io.jsonwebtoken.impl.compression.DeflateCompressionCodec + * @see io.jsonwebtoken.impl.compression.GzipCompressionCodec */ public interface CompressionCodec { diff --git a/src/main/java/io/jsonwebtoken/Header.java b/src/main/java/io/jsonwebtoken/Header.java index f4b5cfbf..81068c73 100644 --- a/src/main/java/io/jsonwebtoken/Header.java +++ b/src/main/java/io/jsonwebtoken/Header.java @@ -103,8 +103,25 @@ public interface Header> extends Map { */ T setContentType(String cty); + /** + * Returns the JWT calg (Compression Algorithm) header value or {@code null} if not present. + * + * @return the {@code calg} header parameter value or {@code null} if not present. + * @since 0.5.2 + */ String getCompressionAlgorithm(); - T setCompressionAlgorithm(String compressionAlgorithm); + /** + * Sets the JWT calg (Compression Algorithm) header parameter value. A {@code null} value will remove + * the property from the JSON map. + *

+ *

The compression algorithm is NOT part of the