Skip to content

Commit

Permalink
- Implemented new Base64 encoder forked from MigBase64 to guarantee d…
Browse files Browse the repository at this point in the history
…eterministic behavior on all JDK and Android platforms

- Allowed pluggable Encoder/Decoder for JWT building and parsing via new Encoder/Decoder and JwtBuilder#base64UrlEncodeWith
  and JwtParser#base64UrlDecodeWith methods respectively
- added RFC 4648 Base64 test vectors per code review
- Added tests for all new code to retain 100% code coverage, verified by Clover and Coveralls
- Enabled oraclejdk10 and openjdk10 builds in TravisCI
- Replaced gmaven plugin with gmavenplus to work on JDK >= 9
- Upgraded surefire and failsafe plugins to 2.22.0 to ensure build works on JDK >= 10
- Ensured JavaDoc linter wouldn't fail the build for JDK >= 8 (was previously only 1.8)
  • Loading branch information
lhazlewood committed Jul 8, 2018
1 parent bf6423b commit 4b9a3ca
Show file tree
Hide file tree
Showing 53 changed files with 1,720 additions and 271 deletions.
4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ jdk:
- openjdk7
- oraclejdk8
- oraclejdk9
- oraclejdk10
- openjdk10
- oraclejdk-ea
- openjdk11

before_install:
- export BUILD_COVERAGE="$([ $TRAVIS_JDK_VERSION == 'oraclejdk8' ] && echo 'true')"
Expand Down
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ Jwts.builder().claim("foo", "someReallyLongDataString...")
.compact();
```

This will set a new `calg` header with the name of the compression algorithm used so that parsers can see that value and decompress accordingly.
This will set a new `zip` header with the name of the compression algorithm used so that parsers can see that value and decompress accordingly.

The default parser implementation will automatically decompress DEFLATE or GZIP compressed bodies, so you don't need to set anything on the parser - it looks like normal:

Expand All @@ -155,7 +155,7 @@ Jwts.builder().claim("foo", "someReallyLongDataString...")
.compact();
```

You will then need to specify a `CompressionCodecResolver` on the parser, so you can inspect the `calg` header and return your custom codec when discovered:
You will then need to specify a `CompressionCodecResolver` on the parser, so you can inspect the `zip` header and return your custom codec when discovered:

```java
Jwts.parser().setSigningKey(key)
Expand Down
32 changes: 32 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
The io.jsonwebtoken.codec.impl.Base64 implementation is based on MigBase64 with modifications for Base64 URL support. This
class's copyright and license notice have been retained and are repeated here per that code's requirements:

**** BEGIN MIGBASE64 NOTICE *****
Licence (BSD):
==============

Copyright (c) 2004, Mikael Grev, MiG InfoCom AB. (base64 @ miginfocom . com)
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this list
of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or other
materials provided with the distribution.
Neither the name of the MiG InfoCom AB nor the names of its contributors may be
used to endorse or promote products derived from this software without specific
prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
OF SUCH DAMAGE.
**** END MIGBASE64 NOTICE *****
39 changes: 14 additions & 25 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,13 @@
<bouncycastle.version>1.56</bouncycastle.version>

<!-- Test Dependencies: Only required for testing when building. Not required by users at runtime: -->
<groovy.version>2.4.11</groovy.version>
<groovy.version>2.4.15</groovy.version>
<logback.version>1.2.3</logback.version>
<easymock.version>3.5</easymock.version>
<junit.version>4.12</junit.version>
<powermock.version>2.0.0-beta.5</powermock.version> <!-- necessary for Java 9 support -->
<failsafe.plugin.version>2.20.1</failsafe.plugin.version>
<surefire.plugin.version>2.20.1</surefire.plugin.version>
<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>

</properties>
Expand Down Expand Up @@ -233,35 +233,24 @@
</plugin>
<!-- Allow for writing tests in Groovy: -->
<plugin>
<groupId>org.codehaus.gmaven</groupId>
<artifactId>gmaven-plugin</artifactId>
<version>1.5</version>
<configuration>
<providerSelection>2.0</providerSelection>
<source />
</configuration>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<version>1.6.1</version>
<executions>
<execution>
<goals>
<goal>addSources</goal>
<goal>addTestSources</goal>
<goal>generateStubs</goal>
<goal>compile</goal>
<goal>generateTestStubs</goal>
<goal>testCompile</goal>
<goal>compileTests</goal>
<goal>removeStubs</goal>
<goal>removeTestStubs</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.codehaus.gmaven.runtime</groupId>
<artifactId>gmaven-runtime-2.0</artifactId>
<version>1.5</version>
<exclusions>
<exclusion>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
Expand Down Expand Up @@ -406,12 +395,12 @@
</build>
<profiles>
<profile>
<id>jdk8</id>
<id>nonJDK7</id>
<activation>
<jdk>1.8</jdk>
<jdk>[1.8,)</jdk>
</activation>
<properties>
<!-- Turn off JDK 8's lint checks: -->
<!-- Turn off JDK >= 8's lint checks: -->
<additionalparam>-Xdoclint:none</additionalparam>
</properties>
</profile>
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/io/jsonwebtoken/CompressionCodec.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@
public interface CompressionCodec {

/**
* The algorithm name to use as the JWT's {@code calg} header value.
* The algorithm name to use as the JWT's {@code zip} header value.
*
* @return the algorithm name to use as the JWT's {@code calg} header value.
* @return the algorithm name to use as the JWT's {@code zip} header value.
*/
String getAlgorithmName();

Expand Down
8 changes: 4 additions & 4 deletions src/main/java/io/jsonwebtoken/CompressionCodecResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
package io.jsonwebtoken;

/**
* Looks for a JWT {@code calg} header, and if found, returns the corresponding {@link CompressionCodec} the parser
* Looks for a JWT {@code zip} header, and if found, returns the corresponding {@link CompressionCodec} the parser
* can use to decompress the JWT body.
*
* <p>JJWT's default {@link JwtParser} implementation supports both the
Expand All @@ -34,12 +34,12 @@
public interface CompressionCodecResolver {

/**
* Looks for a JWT {@code calg} header, and if found, returns the corresponding {@link CompressionCodec} the parser
* Looks for a JWT {@code zip} header, and if found, returns the corresponding {@link CompressionCodec} the parser
* can use to decompress the JWT body.
*
* @param header of the JWT
* @return CompressionCodec matching the {@code calg} header, or null if there is no {@code calg} header.
* @throws CompressionException if a {@code calg} header value is found and not supported.
* @return CompressionCodec matching the {@code zip} header, or null if there is no {@code zip} header.
* @throws CompressionException if a {@code zip} header value is found and not supported.
*/
CompressionCodec resolveCompressionCodec(Header header) throws CompressionException;

Expand Down
10 changes: 5 additions & 5 deletions src/main/java/io/jsonwebtoken/Header.java
Original file line number Diff line number Diff line change
Expand Up @@ -109,24 +109,24 @@ public interface Header<T extends Header<T>> extends Map<String,Object> {
T setContentType(String cty);

/**
* Returns the JWT <code>calg</code> (Compression Algorithm) header value or {@code null} if not present.
* Returns the JWT <code>zip</code> (Compression Algorithm) header value or {@code null} if not present.
*
* @return the {@code calg} header parameter value or {@code null} if not present.
* @return the {@code zip} header parameter value or {@code null} if not present.
* @since 0.6.0
*/
String getCompressionAlgorithm();

/**
* Sets the JWT <code>calg</code> (Compression Algorithm) header parameter value. A {@code null} value will remove
* Sets the JWT <code>zip</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.
* @param zip the JWT compression algorithm {@code zip} value or {@code null} to remove the property from the JSON map.
* @since 0.6.0
*/
T setCompressionAlgorithm(String calg);
T setCompressionAlgorithm(String zip);

}
39 changes: 39 additions & 0 deletions src/main/java/io/jsonwebtoken/JwtBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package io.jsonwebtoken;

import io.jsonwebtoken.codec.Encoder;

import java.security.Key;
import java.util.Date;
import java.util.Map;
Expand Down Expand Up @@ -345,11 +347,36 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
* <p>This is a convenience method: the string argument is first BASE64-decoded to a byte array and this resulting
* byte array is used to invoke {@link #signWith(SignatureAlgorithm, byte[])}.</p>
*
* <h4>Deprecation Notice: Deprecated as of 0.10.0, will be removed in 1.0.0</h4>
*
* <p>This method has been deprecated because the {@code key} argument for this method can be confusing: keys for
* cryptographic operations are always binary (byte arrays), and many people were confused as to how bytes were
* obtained from the String argument.</p>
*
* <p>This method always expected a String argument that was effectively the same as the result of the following
* (pseudocode):</p>
*
* <p>{@code String base64EncodedSecretKey = base64Encode(secretKeyBytes);}</p>
*
* <p>However, a non-trivial number of JJWT users were confused by the method signature and attempted to
* use raw password strings as the key argument - for example {@code signWith(HS256, myPassword)} - which is
* almost always incorrect for cryptographic hashes and can produce erroneous or insecure results.</p>
*
* <p>See this
* <a href="https://stackoverflow.com/questions/40252903/static-secret-as-byte-key-or-string/40274325#40274325">
* StackOverflow answer</a> explaining why raw (non-base64-encoded) strings are almost always incorrect for
* signature operations.</p>
*
* <p>Finally, please use the {@link #signWith(SignatureAlgorithm, Key)} method, as this method and the
* {@code byte[]} variant will be removed before the 1.0.0 release.</p>
*
* @param alg the JWS algorithm to use to digitally sign the JWT, thereby producing a JWS.
* @param base64EncodedSecretKey the BASE64-encoded algorithm-specific signing key to use to digitally sign the
* JWT.
* @return the builder for method chaining.
* @deprecated as of 0.10.0 - use {@link #signWith(SignatureAlgorithm, Key)} instead.
*/
@Deprecated
JwtBuilder signWith(SignatureAlgorithm alg, String base64EncodedSecretKey);

/**
Expand Down Expand Up @@ -387,6 +414,18 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
*/
JwtBuilder compressWith(CompressionCodec codec);

/**
* Perform Base64Url encoding with the specified Encoder.
*
* <p>JJWT uses a spec-compliant encoder that works on all supported JDK versions, but you may call this method
* to specify a different encoder if you desire.</p>
*
* @param base64UrlEncoder the encoder to use when Base64Url-encoding
* @return the builder for method chaining.
* @since 0.10.0
*/
JwtBuilder base64UrlEncodeWith(Encoder<byte[], String> base64UrlEncoder);

/**
* 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
46 changes: 41 additions & 5 deletions src/main/java/io/jsonwebtoken/JwtParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package io.jsonwebtoken;

import io.jsonwebtoken.codec.Decoder;
import io.jsonwebtoken.impl.DefaultClock;

import java.security.Key;
Expand Down Expand Up @@ -164,20 +165,43 @@ public interface JwtParser {
/**
* Sets the signing key used to verify any discovered JWS digital signature. If the specified JWT string is not
* a JWS (no signature), this key is not used.
* <p>
*
* <p>Note that this key <em>MUST</em> be a valid key for the signature algorithm found in the JWT header
* (as the {@code alg} header parameter).</p>
* <p>
*
* <p>This method overwrites any previously set key.</p>
* <p>
*
* <p>This is a convenience method: the string argument is first BASE64-decoded to a byte array and this resulting
* byte array is used to invoke {@link #setSigningKey(byte[])}.</p>
*
* @param base64EncodedKeyBytes the BASE64-encoded algorithm-specific signature verification key to use to validate
* <h4>Deprecation Notice: Deprecated as of 0.10.0, will be removed in 1.0.0</h4>
*
* <p>This method has been deprecated because the {@code key} argument for this method can be confusing: keys for
* cryptographic operations are always binary (byte arrays), and many people were confused as to how bytes were
* obtained from the String argument.</p>
*
* <p>This method always expected a String argument that was effectively the same as the result of the following
* (pseudocode):</p>
*
* <p>{@code String base64EncodedSecretKey = base64Encode(secretKeyBytes);}</p>
*
* <p>However, a non-trivial number of JJWT users were confused by the method signature and attempted to
* use raw password strings as the key argument - for example {@code setSigningKey(myPassword)} - which is
* almost always incorrect for cryptographic hashes and can produce erroneous or insecure results.</p>
*
* <p>See this
* <a href="https://stackoverflow.com/questions/40252903/static-secret-as-byte-key-or-string/40274325#40274325">
* StackOverflow answer</a> explaining why raw (non-base64-encoded) strings are almost always incorrect for
* signature operations.</p>
*
* <p>Finally, please use the {@link #setSigningKey(Key) setSigningKey(Key)} instead, as this method and the
* {@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.
* @return the parser for method chaining.
*/
JwtParser setSigningKey(String base64EncodedKeyBytes);
JwtParser setSigningKey(String base64EncodedSecretKey);

/**
* Sets the signing key used to verify any discovered JWS digital signature. If the specified JWT string is not
Expand Down Expand Up @@ -246,6 +270,18 @@ public interface JwtParser {
*/
JwtParser setCompressionCodecResolver(CompressionCodecResolver compressionCodecResolver);

/**
* Perform Base64Url decoding with the specified Decoder
*
* <p>JJWT uses a spec-compliant decoder that works on all supported JDK versions, but you may call this method
* to specify a different decoder if you desire.</p>
*
* @param base64UrlDecoder the decoder to use when Base64Url-decoding
* @return the parser for method chaining.
* @since 0.10.0
*/
JwtParser base64UrlDecodeWith(Decoder<String, byte[]> base64UrlDecoder);

/**
* Returns {@code true} if the specified JWT compact string represents a signed JWT (aka a 'JWS'), {@code false}
* otherwise.
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/io/jsonwebtoken/codec/CodecException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.jsonwebtoken.codec;

import io.jsonwebtoken.JwtException;

/**
* @since 0.10.0
*/
public class CodecException extends JwtException {

public CodecException(String message, Throwable cause) {
super(message, cause);
}
}
18 changes: 18 additions & 0 deletions src/main/java/io/jsonwebtoken/codec/Decoder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.jsonwebtoken.codec;

import io.jsonwebtoken.codec.impl.Base64Decoder;
import io.jsonwebtoken.codec.impl.Base64UrlDecoder;
import io.jsonwebtoken.codec.impl.ExceptionPropagatingDecoder;

/**
* @param <T>
* @param <R>
* @since 0.10.0
*/
public interface Decoder<T, R> {

Decoder<String, byte[]> BASE64 = new ExceptionPropagatingDecoder<>(new Base64Decoder());
Decoder<String, byte[]> BASE64URL = new ExceptionPropagatingDecoder<>(new Base64UrlDecoder());

R decode(T t) throws DecodingException;
}
11 changes: 11 additions & 0 deletions src/main/java/io/jsonwebtoken/codec/DecodingException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.jsonwebtoken.codec;

/**
* @since 0.10.0
*/
public class DecodingException extends CodecException {

public DecodingException(String message, Throwable cause) {
super(message, cause);
}
}
Loading

0 comments on commit 4b9a3ca

Please sign in to comment.