Merge pull request #54 from josebarrueta/Issue-52

Issue 52
This commit is contained in:
Les Hazlewood 2015-10-12 12:50:58 -07:00
commit bb471be0e9
21 changed files with 819 additions and 10 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
*.class *.class
.DS_Store
# Mobile Tools for Java (J2ME) # Mobile Tools for Java (J2ME)
.mtj.tmp/ .mtj.tmp/

View File

@ -0,0 +1,46 @@
/*
* 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;
/**
* 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 {
/**
* 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);
}

View File

@ -0,0 +1,31 @@
/*
* 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;
/**
* 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);
}

View File

@ -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);
}
}

View File

@ -48,6 +48,9 @@ public interface Header<T extends Header<T>> extends Map<String,Object> {
/** JWT {@code Content Type} header parameter name: <code>"cty"</code> */ /** JWT {@code Content Type} header parameter name: <code>"cty"</code> */
public static final String CONTENT_TYPE = "cty"; public static final String CONTENT_TYPE = "cty";
/** JWT {@code Compression Algorithm} header parameter name: <code>"calg"</code> */
public static final String COMPRESSION_ALGORITHM = "calg";
/** /**
* Returns the <a href="https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25#section-5.1"> * Returns the <a href="https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25#section-5.1">
* <code>typ</code></a> (type) header value or {@code null} if not present. * <code>typ</code></a> (type) header value or {@code null} if not present.
@ -100,4 +103,25 @@ public interface Header<T extends Header<T>> extends Map<String,Object> {
*/ */
T setContentType(String cty); T setContentType(String cty);
/**
* Returns the JWT <code>calg</code> (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();
/**
* Sets the JWT <code>calg</code> (Compression Algorithm) header parameter value. A {@code null} value will remove
* the property from the JSON map.
* <p>
* <p>The compression algorithm is NOT part of the <a href="https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25>JWT specification</a>
* and must be used carefully since, is not expected that other libraries (including previous versions of this one)
* be able to deserialize a compressed JTW body correctly. </p>
*
* @param calg the JWT compression algorithm {@code calg} value or {@code null} to remove the property from the JSON map.
* @since 0.5.2
*/
T setCompressionAlgorithm(String calg);
} }

View File

@ -349,6 +349,18 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
*/ */
JwtBuilder signWith(SignatureAlgorithm alg, Key key); JwtBuilder signWith(SignatureAlgorithm alg, Key key);
/**
* Compresses the JWT body using the {@link CompressionCodec} passed as argument.
*
* Note: Compression is not part of the Json Web Token specification and is not expected that other libraries (including
* older versions of this one) are able to consume a compressed JWT body correctly.
*
* @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 * 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

@ -205,6 +205,16 @@ public interface JwtParser {
*/ */
JwtParser setSigningKeyResolver(SigningKeyResolver signingKeyResolver); JwtParser setSigningKeyResolver(SigningKeyResolver signingKeyResolver);
/**
* Sets the {@link CompressionCodecResolver} used to acquire the {@link CompressionCodec} that should be used to verify
* a decompress the JWT body. If the parsed JWT is not compressed, this resolver si not used.
*
* @param compressionCodecResolver the compression codec resolver used to decompress the JWT body.
* @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} * Returns {@code true} if the specified JWT compact string represents a signed JWT (aka a 'JWS'), {@code false}
* otherwise. * otherwise.

View File

@ -51,4 +51,16 @@ public class DefaultHeader<T extends Header<T>> extends JwtMap implements Header
setValue(CONTENT_TYPE, cty); setValue(CONTENT_TYPE, cty);
return (T)this; return (T)this;
} }
@Override
public String getCompressionAlgorithm() {
return getString(COMPRESSION_ALGORITHM);
}
@Override
public T setCompressionAlgorithm(String compressionAlgorithm) {
setValue(COMPRESSION_ALGORITHM, compressionAlgorithm);
return (T) this;
}
} }

View File

@ -18,6 +18,7 @@ package io.jsonwebtoken.impl;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import io.jsonwebtoken.CompressionCodec;
import io.jsonwebtoken.Header; import io.jsonwebtoken.Header;
import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.JwtBuilder;
@ -48,6 +49,8 @@ public class DefaultJwtBuilder implements JwtBuilder {
private Key key; private Key key;
private byte[] keyBytes; private byte[] keyBytes;
private CompressionCodec compressionCodec;
@Override @Override
public JwtBuilder setHeader(Header header) { public JwtBuilder setHeader(Header header) {
this.header = header; this.header = header;
@ -113,6 +116,13 @@ public class DefaultJwtBuilder implements JwtBuilder {
return this; return this;
} }
@Override
public JwtBuilder compressWith(CompressionCodec compressionCodec) {
Assert.notNull(compressionCodec, "compressionCodec cannot be null");
this.compressionCodec = compressionCodec;
return this;
}
@Override @Override
public JwtBuilder setPayload(String payload) { public JwtBuilder setPayload(String payload) {
this.payload = payload; this.payload = payload;
@ -279,11 +289,30 @@ public class DefaultJwtBuilder implements JwtBuilder {
jwsHeader.setAlgorithm(SignatureAlgorithm.NONE.getValue()); jwsHeader.setAlgorithm(SignatureAlgorithm.NONE.getValue());
} }
if (compressionCodec != null) {
jwsHeader.setCompressionAlgorithm(compressionCodec.getAlgorithmName());
}
String base64UrlEncodedHeader = base64UrlEncode(jwsHeader, "Unable to serialize header to json."); String base64UrlEncodedHeader = base64UrlEncode(jwsHeader, "Unable to serialize header to json.");
String base64UrlEncodedBody = this.payload != null ? 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) : TextCodec.BASE64URL.encode(this.payload) :
base64UrlEncode(claims, "Unable to serialize claims object to json."); base64UrlEncode(claims, "Unable to serialize claims object to json.");
}
String jwt = base64UrlEncodedHeader + JwtParser.SEPARATOR_CHAR + base64UrlEncodedBody; String jwt = base64UrlEncodedHeader + JwtParser.SEPARATOR_CHAR + base64UrlEncodedBody;
@ -311,17 +340,18 @@ public class DefaultJwtBuilder implements JwtBuilder {
} }
protected String base64UrlEncode(Object o, String errMsg) { protected String base64UrlEncode(Object o, String errMsg) {
String s; byte[] bytes;
try { try {
s = toJson(o); bytes = toJson(o);
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
throw new IllegalStateException(errMsg, 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);
} }
} }

View File

@ -18,6 +18,8 @@ package io.jsonwebtoken.impl;
import com.fasterxml.jackson.databind.ObjectMapper; 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.CompressionCodec;
import io.jsonwebtoken.CompressionCodecResolver;
import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Header; import io.jsonwebtoken.Header;
import io.jsonwebtoken.IncorrectClaimException; import io.jsonwebtoken.IncorrectClaimException;
@ -34,7 +36,9 @@ import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.PrematureJwtException; import io.jsonwebtoken.PrematureJwtException;
import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException; import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.SigningKeyResolver;
import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.UnsupportedJwtException;
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.lang.Assert; import io.jsonwebtoken.lang.Assert;
@ -62,6 +66,8 @@ public class DefaultJwtParser implements JwtParser {
private SigningKeyResolver signingKeyResolver; private SigningKeyResolver signingKeyResolver;
private CompressionCodecResolver compressionCodecResolver = new DefaultCompressionCodecResolver();
Claims expectedClaims = new DefaultClaims(); Claims expectedClaims = new DefaultClaims();
@Override @Override
@ -150,6 +156,13 @@ public class DefaultJwtParser implements JwtParser {
return this; return this;
} }
@Override
public JwtParser setCompressionCodecResolver(CompressionCodecResolver compressionCodecResolver) {
Assert.notNull(compressionCodecResolver, "compressionCodecResolver cannot be null.");
this.compressionCodecResolver = compressionCodecResolver;
return this;
}
@Override @Override
public boolean isSigned(String jwt) { public boolean isSigned(String jwt) {
@ -221,6 +234,8 @@ public class DefaultJwtParser implements JwtParser {
// =============== Header ================= // =============== Header =================
Header header = null; Header header = null;
CompressionCodec compressionCodec = null;
if (base64UrlEncodedHeader != null) { if (base64UrlEncodedHeader != null) {
String origValue = TextCodec.BASE64URL.decodeToString(base64UrlEncodedHeader); String origValue = TextCodec.BASE64URL.decodeToString(base64UrlEncodedHeader);
Map<String, Object> m = readValue(origValue); Map<String, Object> m = readValue(origValue);
@ -230,10 +245,18 @@ public class DefaultJwtParser implements JwtParser {
} else { } else {
header = new DefaultHeader(m); header = new DefaultHeader(m);
} }
compressionCodec = compressionCodecResolver.resolveCompressionCodec(header);
} }
// =============== Body ================= // =============== 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; Claims claims = null;

View File

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

View File

@ -0,0 +1,37 @@
/*
* 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 interface CompressionCodecs {
/**
* Codec implementing the <a href="https://en.wikipedia.org/wiki/DEFLATE">deflate</a> compression algorithm
*/
CompressionCodec DEFLATE = new DeflateCompressionCodec();
/**
* Codec implementing the <a href="https://en.wikipedia.org/wiki/Gzip">gzip</a> compression algorithm
*/
CompressionCodec GZIP = new GzipCompressionCodec();
}

View File

@ -0,0 +1,56 @@
/*
* 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;
/**
* Default implementation of {@link CompressionCodecResolver}. This implementation will resolve DEF to
* {@link DeflateCompressionCodec} and GZIP to {@link GzipCompressionCodec}.
*
* @since 0.5.2
*/
public class DefaultCompressionCodecResolver implements CompressionCodecResolver {
@Override
public CompressionCodec resolveCompressionCodec(Header header) {
String cmpAlg = getAlgorithmFromHeader(header);
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 + "'");
}
private String getAlgorithmFromHeader(Header header) {
Assert.notNull(header, "header cannot be null.");
return header.getCompressionAlgorithm();
}
}

View File

@ -0,0 +1,73 @@
/*
* 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.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;
/**
* Codec implementing the <a href="https://en.wikipedia.org/wiki/DEFLATE">deflate</a> compression algorithm
* @since 0.5.2
*/
public class DeflateCompressionCodec extends BaseCompressionCodec {
private static final String DEFLATE = "DEF";
@Override
public String getAlgorithmName() {
return DEFLATE;
}
@Override
public byte[] doCompress(byte[] payload) throws IOException {
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();
} finally {
Objects.nullSafeClose(outputStream, deflaterOutputStream);
}
}
@Override
public byte[] doDecompress(byte[] compressed) throws IOException {
InflaterOutputStream inflaterOutputStream = null;
ByteArrayOutputStream decompressedOutputStream = null;
try {
decompressedOutputStream = new ByteArrayOutputStream();
inflaterOutputStream = new InflaterOutputStream(decompressedOutputStream);
inflaterOutputStream.write(compressed);
inflaterOutputStream.flush();
return decompressedOutputStream.toByteArray();
} finally {
Objects.nullSafeClose(decompressedOutputStream, inflaterOutputStream);
}
}
}

View File

@ -0,0 +1,74 @@
/*
* 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.lang.Objects;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
/**
* Codec implementing the <a href="https://en.wikipedia.org/wiki/Gzip">gzip</a> compression algorithm
*
* @since 0.5.2
*/
public class GzipCompressionCodec extends BaseCompressionCodec implements CompressionCodec {
private static final String GZIP = "GZIP";
@Override
public String getAlgorithmName() {
return GZIP;
}
@Override
protected byte[] doDecompress(byte[] compressed) throws IOException {
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();
} 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);
}
}
}

View File

@ -15,6 +15,8 @@
*/ */
package io.jsonwebtoken.lang; package io.jsonwebtoken.lang;
import java.io.Closeable;
import java.io.IOException;
import java.lang.reflect.Array; import java.lang.reflect.Array;
import java.util.Arrays; import java.util.Arrays;
@ -905,4 +907,19 @@ public abstract class Objects {
return sb.toString(); 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.
}
}
}
}
} }

View File

@ -15,6 +15,7 @@
*/ */
package io.jsonwebtoken.lang; package io.jsonwebtoken.lang;
import java.nio.charset.Charset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
@ -40,6 +41,8 @@ public abstract class Strings {
private static final char EXTENSION_SEPARATOR = '.'; private static final char EXTENSION_SEPARATOR = '.';
public static final Charset UTF_8 = Charset.forName("UTF-8");
//--------------------------------------------------------------------- //---------------------------------------------------------------------
// General convenience methods for working with Strings // General convenience methods for working with Strings
//--------------------------------------------------------------------- //---------------------------------------------------------------------

View File

@ -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()
}
}

View File

@ -19,9 +19,13 @@ import com.fasterxml.jackson.databind.ObjectMapper
import io.jsonwebtoken.impl.DefaultHeader import io.jsonwebtoken.impl.DefaultHeader
import io.jsonwebtoken.impl.DefaultJwsHeader import io.jsonwebtoken.impl.DefaultJwsHeader
import io.jsonwebtoken.impl.TextCodec 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.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.lang.Strings
import org.junit.Test import org.junit.Test
import javax.crypto.Mac import javax.crypto.Mac
@ -108,6 +112,7 @@ class JwtsTest {
def token = Jwts.parser().parse(jwt); def token = Jwts.parser().parse(jwt);
//noinspection GrEqualsBetweenInconvertibleTypes
assert token.body == claims assert token.body == claims
} }
@ -186,6 +191,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 @Test
void testConvenienceIssuer() { void testConvenienceIssuer() {
String compact = Jwts.builder().setIssuer("Me").compact(); String compact = Jwts.builder().setIssuer("Me").compact();
@ -320,6 +335,140 @@ class JwtsTest {
assertNull claims.getId() 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() {
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 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 = header.getCompressionAlgorithm()
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() {
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 @Test
void testHS256() { void testHS256() {
testHmac(SignatureAlgorithm.HS256); testHmac(SignatureAlgorithm.HS256);
@ -571,6 +720,7 @@ class JwtsTest {
def token = Jwts.parser().setSigningKey(key).parse(jwt); def token = Jwts.parser().setSigningKey(key).parse(jwt);
assert [alg: alg.name()] == token.header assert [alg: alg.name()] == token.header
//noinspection GrEqualsBetweenInconvertibleTypes
assert token.body == claims assert token.body == claims
} }
@ -585,6 +735,7 @@ class JwtsTest {
def token = Jwts.parser().setSigningKey(key).parse(jwt) def token = Jwts.parser().setSigningKey(key).parse(jwt)
assert token.header == [alg: alg.name()] assert token.header == [alg: alg.name()]
//noinspection GrEqualsBetweenInconvertibleTypes
assert token.body == claims assert token.body == claims
} }
@ -606,6 +757,7 @@ class JwtsTest {
def token = Jwts.parser().setSigningKey(key).parse(jwt); def token = Jwts.parser().setSigningKey(key).parse(jwt);
assert token.header == [alg: alg.name()] assert token.header == [alg: alg.name()]
//noinspection GrEqualsBetweenInconvertibleTypes
assert token.body == claims assert token.body == claims
} }
} }

View File

@ -173,7 +173,7 @@ class DefaultJwtBuilderTest {
def b = new DefaultJwtBuilder() { def b = new DefaultJwtBuilder() {
@Override @Override
protected String toJson(Object o) throws JsonProcessingException { protected byte[] toJson(Object o) throws JsonProcessingException {
throw new JsonMappingException('foo') throw new JsonMappingException('foo')
} }
} }

View File

@ -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]);
}
}