diff --git a/.gitignore b/.gitignore index 611af4c7..bc177c84 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ target/ *.iml *.iws +.classpath +.project +.settings diff --git a/.travis.yml b/.travis.yml index 8ef0212b..197d2a00 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,4 +10,4 @@ before_install: install: echo "No need to run mvn install -DskipTests then mvn install. Running mvn install." script: mvn install after_success: - - test -z "$BUILD_COVERAGE" || mvn clean cobertura:cobertura coveralls:report + - test -z "$BUILD_COVERAGE" || mvn clean test jacoco:report coveralls:report diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..68022b8e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,302 @@ +## Release Notes + +### 0.7.0 + +This is a minor feature enhancement and bugfix release. One of the bug fixes is particularly important if using +elliptic curve signatures, please see below. + +#### Elliptic Curve Signature Length Bug Fix + +Previous versions of JJWT safely calculated and verified Elliptic Curve signatures (no security risks), however, the + signatures were encoded using the JVM's default ASN.1/DER format. The JWS specification however +requires EC signatures to be in a R + S format. JJWT >= 0.7.0 now correctly represents newly computed EC signatures in +this spec-compliant format. + +What does this mean for you? + +Signatures created from previous JJWT versions can still be verified, so your existing tokens will still be parsed +correctly. HOWEVER, new JWTs with EC signatures created by JJWT >= 0.7.0 are now spec compliant and therefore can only +be verified by JJWT >= 0.7.0 (or any other spec compliant library). + +**This means that if you generate JWTs using Elliptic Curve Signatures after upgrading to JJWT >= 0.7.0, you _must_ +also upgrade any applications that parse these JWTs to upgrade to JJWT >= 0.7.0 as well.** + +#### Clock Skew Support + +When parsing a JWT, you might find that `exp` or `nbf` claims fail because the clock on the parsing machine is not +perfectly in sync with the clock on the machine that created the JWT. You can now account for these differences +(usually no more than a few minutes) when parsing using the new `setAllowedClockSkewSeconds` method on the parser. +For example: + +```java +long seconds = 3 * 60; //3 minutes +Jwts.parser().setAllowedClockSkewSeconds(seconds).setSigningKey(key).parseClaimsJws(jwt); +``` + +This ensures that clock differences between machines can be ignored. Two or three minutes should be more than enough; it +would be very strange if a machine's clock was more than 5 minutes difference from most atomic clocks around the world. + +#### Custom Clock Support + +Timestamps created during parsing can now be obtained via a custom time source via an implementation of + the new `io.jsonwebtoken.Clock` interface. The default implementation simply returns `new Date()` to reflect the time + when parsing occurs, as most would expect. However, supplying your own clock could be useful, especially during test + cases to guarantee deterministic behavior. + +#### Android RSA Private Key Support + +Previous versions of JJWT required RSA private keys to implement `java.security.interfaces.RSAPrivateKey`, but Android +6 RSA private keys do not implement this interface. JJWT now asserts that RSA keys are instances of both +`java.security.interfaces.RSAKey` and `java.security.PrivateKey` which should work fine on both Android and all other +'standard' JVMs as well. + +#### Library version updates + +The few dependencies JWWT has (e.g. Jackson) have been updated to their latest stable versions at the time of release. + +#### Issue List + +For all completed issues, please see the [0.7.0 Milestone List](https://github.com/jwtk/jjwt/milestone/7?closed=1) + +### 0.6.0 + +#### Enforce JWT Claims when Parsing + +You can now enforce that JWT claims have expected values when parsing a compact JWT string. + +For example, let's say that you require that the JWT you are parsing has a specific `sub` (subject) value, +otherwise you may not trust the token. You can do that by using one of the `require` methods on the parser builder: + +```java +try { + Jwts.parser().requireSubject("jsmith").setSigningKey(key).parseClaimsJws(s); +} catch(InvalidClaimException ice) { + // the sub field was missing or did not have a 'jsmith' value +} +``` + +If it is important to react to a missing vs an incorrect value, instead of catching `InvalidClaimException`, you can catch either `MissingClaimException` or `IncorrectClaimException`: + +```java +try { + Jwts.parser().requireSubject("jsmith").setSigningKey(key).parseClaimsJws(s); +} catch(MissingClaimException mce) { + // the parsed JWT did not have the sub field +} catch(IncorrectClaimException ice) { + // the parsed JWT had a sub field, but its value was not equal to 'jsmith' +} +``` + +You can also require custom fields by using the `require(fieldName, requiredFieldValue)` method - for example: + +```java +try { + Jwts.parser().require("myfield", "myRequiredValue").setSigningKey(key).parseClaimsJws(s); +} catch(InvalidClaimException ice) { + // the 'myfield' field was missing or did not have a 'myRequiredValue' value +} +``` +(or, again, you could catch either MissingClaimException or IncorrectClaimException instead) + +#### Body Compression + +**This feature is NOT JWT specification compliant**, *but it can be very useful when you parse your own tokens*. + +If your JWT body is large and you have size restrictions (for example, if embedding a JWT in a URL and the URL must be under a certain length for legacy browsers or mail user agents), you may now compress the JWT body using a `CompressionCodec`: + +```java +Jwts.builder().claim("foo", "someReallyLongDataString...") + .compressWith(CompressionCodecs.DEFLATE) // or CompressionCodecs.GZIP + .signWith(SignatureAlgorithm.HS256, key) + .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. + +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: + +```java +Jwts.parser().setSigningKey(key).parseClaimsJws(compact); +``` + +##### Custom Compression Algorithms + +If the DEFLATE or GZIP algorithms are not sufficient for your needs, you can specify your own Compression algorithms by implementing the `CompressionCodec` interface and setting it on the parser: + +```java +Jwts.builder().claim("foo", "someReallyLongDataString...") + .compressWith(new MyCompressionCodec()) + .signWith(SignatureAlgorithm.HS256, key) + .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: + +```java +Jwts.parser().setSigningKey(key) + .setCompressionCodecResolver(new MyCustomCompressionCodecResolver()) + .parseClaimsJws(compact); +``` + +*NOTE*: Because body compression is not JWT specification compliant, you should only enable compression if both your JWT builder and parser are JJWT versions >= 0.6.0, or if you're using another library that implements the exact same functionality. This feature is best reserved for your own use cases - where you both create and later parse the tokens. It will likely cause problems if you compressed a token and expected a 3rd party (who doesn't use JJWT) to parse the token. + +### 0.5.1 + +- Minor [bug](https://github.com/jwtk/jjwt/issues/31) fix [release](https://github.com/jwtk/jjwt/issues?q=milestone%3A0.5.1+is%3Aclosed) that ensures correct Base64 padding in Android runtimes. + +### 0.5 + +- Android support! Android's built-in Base64 codec will be used if JJWT detects it is running in an Android environment. Other than Base64, all other parts of JJWT were already Android-compliant. Now it is fully compliant. + +- Elliptic Curve signature algorithms! `SignatureAlgorithm.ES256`, `ES384` and `ES512` are now supported. + +- Super convenient key generation methods, so you don't have to worry how to do this safely: + - `MacProvider.generateKey(); //or generateKey(SignatureAlgorithm)` + - `RsaProvider.generateKeyPair(); //or generateKeyPair(sizeInBits)` + - `EllipticCurveProvider.generateKeyPair(); //or generateKeyPair(SignatureAlgorithm)` + + The `generate`* methods that accept an `SignatureAlgorithm` argument know to generate a key of sufficient strength that reflects the specified algorithm strength. + +Please see the full [0.5 closed issues list](https://github.com/jwtk/jjwt/issues?q=milestone%3A0.5+is%3Aclosed) for more information. + +### 0.4 + +- [Issue 8](https://github.com/jwtk/jjwt/issues/8): Add ability to find signing key by inspecting the JWS values before verifying the signature. + +This is a handy little feature. If you need to parse a signed JWT (a JWS) and you don't know which signing key was used to sign it, you can now use the new `SigningKeyResolver` concept. + +A `SigningKeyresolver` can inspect the JWS header and body (Claims or String) _before_ the JWS signature is verified. By inspecting the data, you can find the key and return it, and the parser will use the returned key to validate the signature. For example: + +```java +SigningKeyResolver resolver = new MySigningKeyResolver(); + +Jws jws = Jwts.parser().setSigningKeyResolver(resolver).parseClaimsJws(compact); +``` + +The signature is still validated, and the JWT instance will still not be returned if the jwt string is invalid, as expected. You just get to 'see' the JWT data for key discovery before the parser validates. Nice. + +This of course requires that you put some sort of information in the JWS when you create it so that your `SigningKeyResolver` implementation can look at it later and look up the key. The *standard* way to do this is to use the JWS `kid` ('key id') field, for example: + +```java +Jwts.builder().setHeaderParam("kid", your_signing_key_id_NOT_THE_SECRET).build(); +``` + +You could of course set any other header parameter or claims parameter instead of setting `kid` if you want - that's just the default field reserved for signing key identification. If you can locate the signing key based on other information in the header or claims, you don't need to set the `kid` field - just make sure your resolver implementation knows how to look up the key. + +Finally, a nice `SigningKeyResolverAdapter` is provided to allow you to write quick and simple subclasses or anonymous classes instead of having to implement the `SigningKeyResolver` interface directly. For example: + +```java +Jws jws = Jwts.parser().setSigningKeyResolver(new SigningKeyResolverAdapter() { + @Override + public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { + //inspect the header or claims, lookup and return the signing key + String keyId = header.getKeyId(); //or any other field that you need to inspect + return getSigningKey(keyId); //implement me + }}) + .parseClaimsJws(compact); +``` + +### 0.3 + +- [Issue 6](https://github.com/jwtk/jjwt/issues/6): Parsing an expired Claims JWT or JWS (as determined by the `exp` claims field) will now throw an `ExpiredJwtException`. +- [Issue 7](https://github.com/jwtk/jjwt/issues/7): Parsing a premature Claims JWT or JWS (as determined by the `nbf` claims field) will now throw a `PrematureJwtException`. + +### 0.2 + +#### More convenient Claims building + +This release adds convenience methods to the `JwtBuilder` interface so you can set claims directly on the builder without having to create a separate Claims instance/builder, reducing the amount of code you have to write. For example, this: + +```java +Claims claims = Jwts.claims().setSubject("Joe"); + +String compactJwt = Jwts.builder().setClaims(claims).signWith(HS256, key).compact(); +``` + +can now be written as: + +```java +String compactJwt = Jwts.builder().setSubject("Joe").signWith(HS256, key).compact(); +``` + +A Claims instance based on the specified claims will be created and set as the JWT's payload automatically. + +#### Type-safe handling for JWT and JWS with generics + +The following < 0.2 code produced a JWT as expected: + +```java +Jwt jwt = Jwts.parser().setSigningKey(key).parse(compact); +``` + +But you couldn't easily determine if the `jwt` was a `JWT` or `JWS` instance or if the body was a `Claims` instance or a plaintext `String` without resorting to a bunch of yucky `instanceof` checks. In 0.2, we introduce the `JwtHandler` when you don't know the exact format of the compact JWT string ahead of time, and parsing convenience methods when you do. + +##### JwtHandler + +If you do not know the format of the compact JWT string at the time you try to parse it, you can determine what type it is after parsing by providing a `JwtHandler` instance to the `JwtParser` with the new `parse(String compactJwt, JwtHandler handler)` method. For example: + +```java +T returnVal = Jwts.parser().setSigningKey(key).parse(compact, new JwtHandler() { + @Override + public T onPlaintextJwt(Jwt jwt) { + //the JWT parsed was an unsigned plaintext JWT + //inspect it, then return an instance of T (see returnVal above) + } + + @Override + public T onClaimsJwt(Jwt jwt) { + //the JWT parsed was an unsigned Claims JWT + //inspect it, then return an instance of T (see returnVal above) + } + + @Override + public T onPlaintextJws(Jws jws) { + //the JWT parsed was a signed plaintext JWS + //inspect it, then return an instance of T (see returnVal above) + } + + @Override + public T onClaimsJws(Jws jws) { + //the JWT parsed was a signed Claims JWS + //inspect it, then return an instance of T (see returnVal above) + } +}); +``` + +Of course, if you know you'll only have to parse a subset of the above, you can use the `JwtHandlerAdapter` and implement only the methods you need. For example: + +```java +T returnVal = Jwts.parser().setSigningKey(key).parse(plaintextJwt, new JwtHandlerAdapter>() { + @Override + public T onPlaintextJws(Jws jws) { + //the JWT parsed was a signed plaintext JWS + //inspect it, then return an instance of T (see returnVal above) + } + + @Override + public T onClaimsJws(Jws jws) { + //the JWT parsed was a signed Claims JWS + //inspect it, then return an instance of T (see returnVal above) + } +}); +``` + +##### Known Type convenience parse methods + +If, unlike above, you are confident of the compact string format and know which type of JWT or JWS it will produce, you can just use one of the 4 new convenience parsing methods to get exactly the type of JWT or JWS you know exists. For example: + +```java + +//for a known plaintext jwt string: +Jwt jwt = Jwts.parser().parsePlaintextJwt(compact); + +//for a known Claims JWT string: +Jwt jwt = Jwts.parser().parseClaimsJwt(compact); + +//for a known signed plaintext JWT (aka a plaintext JWS): +Jws jws = Jwts.parser().setSigningKey(key).parsePlaintextJws(compact); + +//for a known signed Claims JWT (aka a Claims JWS): +Jws jws = Jwts.parser().setSigningKey(key).parseClaimsJws(compact); + +``` diff --git a/README.md b/README.md index 943ebfc9..10d0e128 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,62 @@ [![Build Status](https://travis-ci.org/jwtk/jjwt.svg?branch=master)](https://travis-ci.org/jwtk/jjwt) -[![Coverage Status](https://coveralls.io/repos/jwtk/jjwt/badge.svg?branch=master)](https://coveralls.io/r/jwtk/jjwt?branch=master) +[![Coverage Status](https://coveralls.io/repos/github/jwtk/jjwt/badge.svg?branch=master)](https://coveralls.io/github/jwtk/jjwt?branch=master) -# Java JWT: JSON Web Token for Java and Android +## Java JWT: JSON Web Token for Java and Android JJWT aims to be the easiest to use and understand library for creating and verifying JSON Web Tokens (JWTs) on the JVM. -JJWT is a 'clean room' implementation based solely on the [JWT](https://tools.ietf.org/html/rfc7519), [JWS](https://tools.ietf.org/html/rfc7515), [JWE](https://tools.ietf.org/html/rfc7516), [JWK](https://tools.ietf.org/html/rfc7517) and [JWA](https://tools.ietf.org/html/rfc7518) RFC specifications. +JJWT is a Java implementation based on the [JWT](https://tools.ietf.org/html/rfc7519), [JWS](https://tools.ietf.org/html/rfc7515), [JWE](https://tools.ietf.org/html/rfc7516), [JWK](https://tools.ietf.org/html/rfc7517) and [JWA](https://tools.ietf.org/html/rfc7518) RFC specifications. + +The library was created by [Stormpath's](http://www.stormpath.com) CTO, [Les Hazlewood](https://github.com/lhazlewood) +and is now maintained by a [community](https://github.com/jwtk/jjwt/graphs/contributors) of contributors. + +[Stormpath](https://stormpath.com/) is a complete authentication and user management API for developers. + +We've also added some convenience extensions that are not part of the specification, such as JWT compression and claim enforcement. + +## What's a JSON Web Token? + +Don't know what a JSON Web Token is? Read on. Otherwise, jump on down to the [Installation](#installation) section. + +JWT is a means of transmitting information between two parties in a compact, verifiable form. + +The bits of information encoded in the body of a JWT are called `claims`. The expanded form of the JWT is in a JSON format, so each `claim` is a key in the JSON object. + +JWTs can be cryptographically signed (making it a [JWS](https://tools.ietf.org/html/rfc7515)) or encrypted (making it a [JWE](https://tools.ietf.org/html/rfc7516)). + +This adds a powerful layer of verifiability to the user of JWTs. The receiver has a high degree of confidence that the JWT has not been tampered with by verifying the signature, for instance. + +The compacted representation of a signed JWT is a string that has three parts, each separated by a `.`: + +``` +eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.ipevRNuRP6HflG8cFKnmUPtypruRC4fb1DWtoLL62SY +``` + +Each section is [base 64](https://en.wikipedia.org/wiki/Base64) encoded. The first section is the header, which at a minimum needs to specify the algorithm used to sign the JWT. The second section is the body. This section has all the claims of this JWT encoded in it. The final section is the signature. It's computed by passing a combination of the header and body through the algorithm specified in the header. + +If you pass the first two sections through a base 64 decoder, you'll get the following (formatting added for clarity): + +`header` +``` +{ + "alg": "HS256" +} +``` + +`body` +``` +{ + "sub": "Joe" +} +``` + +In this case, the information we have is that the HMAC using SHA-256 algorithm was used to sign the JWT. And, the body has a single claim, `sub` with value `Joe`. + +There are a number of standard claims, called [Registered Claims](https://tools.ietf.org/html/rfc7519#section-4.1), in the specification and `sub` (for subject) is one of them. + +To compute the signature, you must know the secret that was used to sign it. In this case, it was the word `secret`. You can see the signature creation is action [here](https://jsfiddle.net/dogeared/2fy2y0yd/11/) (Note: Trailing `=` are lopped off the signature for the JWT). + +Now you know (just about) all you need to know about JWTs. ## Installation @@ -17,7 +68,7 @@ Maven: io.jsonwebtoken jjwt - 0.6.0 + 0.7.0 ``` @@ -25,13 +76,13 @@ Gradle: ```groovy dependencies { - compile 'io.jsonwebtoken:jjwt:0.6.0' + compile 'io.jsonwebtoken:jjwt:0.7.0' } ``` Note: JJWT depends on Jackson 2.x. If you're already using an older version of Jackson in your app, [read this](#olderJackson) -## Usage +## Quickstart Most complexity is hidden behind a convenient and readable builder-based [fluent interface](http://en.wikipedia.org/wiki/Fluent_interface), great for relying on IDE auto-completion to write code quickly. Here's an example: @@ -45,25 +96,38 @@ import java.security.Key; // the key would be read from your application configuration instead. Key key = MacProvider.generateKey(); -String s = Jwts.builder().setSubject("Joe").signWith(SignatureAlgorithm.HS512, key).compact(); +String compactJws = Jwts.builder() + .setSubject("Joe") + .signWith(SignatureAlgorithm.HS512, key) + .compact(); ``` How easy was that!? +In this case, we are *building* a JWT that will have the [registered claim](https://tools.ietf.org/html/rfc7519#section-4.1) `sub` (subject) set to `Joe`. We are signing the JWT using the HMAC using SHA-512 algorithm. finally, we are compacting it into its `String` form. + +The resultant `String` looks like this: + +``` +eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJKb2UifQ.yiV1GWDrQyCeoOswYTf_xvlgsnaVVYJM0mU6rkmRBf2T1MBl3Xh2kZii0Q9BdX5-G0j25Qv2WF4lA6jPl5GKuA +``` + Now let's verify the JWT (you should always discard JWTs that don't match an expected signature): ```java -assert Jwts.parser().setSigningKey(key).parseClaimsJws(s).getBody().getSubject().equals("Joe"); +assert Jwts.parser().setSigningKey(key).parseClaimsJws(compactJws).getBody().getSubject().equals("Joe"); ``` -You have to love one-line code snippets! +There are two things going on here. The `key` from before is being used to validate the signature of the JWT. If it fails to verify the JWT, a `SignatureException` is thrown. Assuming the JWT is validated, we parse out the claims and assert that that subject is set to `Joe`. + +You have to love code one-liners that pack a punch! But what if signature validation failed? You can catch `SignatureException` and react accordingly: ```java try { - Jwts.parser().setSigningKey(key).parseClaimsJws(compactJwt); + Jwts.parser().setSigningKey(key).parseClaimsJws(compactJws); //OK, we can trust this JWT @@ -75,6 +139,8 @@ try { ## Supported Features +### Specification Compliant: + * Creating and parsing plaintext compact JWTs * Creating, parsing and verifying digitally signed compact JWTs (aka JWSs) with all standard JWS algorithms: @@ -89,14 +155,57 @@ try { * PS512: RSASSA-PSS using SHA-512 and MGF1 with SHA-512 * ES256: ECDSA using P-256 and SHA-256 * ES384: ECDSA using P-384 and SHA-384 - * ES512: ECDSA using P-512 and SHA-512 + * ES512: ECDSA using P-521 and SHA-512 + +### Enhancements Beyond the Specification: + +* **Body compression.** If the JWT body is large, you can use a `CompressionCodec` to compress it. Best of all, the JJWT library will automtically decompress and parse the JWT without additional coding. + + ```java + String compactJws = Jwts.builder() + .setSubject("Joe") + .compressWith(CompressionCodecs.DEFLATE) + .signWith(SignatureAlgorithm.HS512, key) + .compact(); + ``` + + If you examine the header section of the `compactJws`, it decodes to this: + + ``` + { + "alg": "HS512", + "zip": "DEF" + } + ``` + + JJWT automatically detects that compression was used by examining the header and will automatically decompress when parsing. No extra coding is needed on your part for decompression. + +* **Require Claims.** When parsing, you can specify that certain claims *must* be present and set to a certain value. + + ```java + try { + Jws claims = Jwts.parser() + .requireSubject("Joe") + .require("hasMotorcycle", true) + .setSigningKey(key) + .parseClaimsJws(compactJws); + } catch (MissingClaimException e) { + + // we get here if the required claim is not present + + } catch (IncorrectClaimException) { + + // we get here if ther required claim has the wrong value + + } + ``` ## Currently Unsupported Features * [Non-compact](https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-31#section-7.2) serialization and parsing. * JWE (Encryption for JWT) -These feature sets will be implemented in a future release when possible. Community contributions are welcome! +These feature sets will be implemented in a future release. Community contributions are welcome! ## Learn More @@ -105,262 +214,18 @@ These feature sets will be implemented in a future release when possible. Commu - [Where to Store Your JWTs - Cookies vs HTML5 Web Storage](https://stormpath.com/blog/where-to-store-your-jwts-cookies-vs-html5-web-storage/) - [Use JWT the Right Way!](https://stormpath.com/blog/jwt-the-right-way/) - [Token Authentication for Java Applications](https://stormpath.com/blog/token-auth-for-java/) - -## Release Notes - -### 0.6.0 - -#### Enforce JWT Claims when Parsing - -You can now enforce that JWT claims have expected values when parsing a compact JWT string. - -For example, let's say that you require that the JWT you are parsing has a specific `sub` (subject) value, -otherwise you may not trust the token. You can do that by using one of the `require` methods on the parser builder: - -```java -try { - Jwts.parser().requireSubject("jsmith").setSigningKey(key).parseClaimsJws(s); -} catch(InvalidClaimException ice) { - // the sub field was missing or did not have a 'jsmith' value -} -``` - -If it is important to react to a missing vs an incorrect value, instead of catching `InvalidClaimException`, you can catch either `MissingClaimException` or `IncorrectClaimException`: - -```java -try { - Jwts.parser().requireSubject("jsmith").setSigningKey(key).parseClaimsJws(s); -} catch(MissingClaimException mce) { - // the parsed JWT did not have the sub field -} catch(IncorrectClaimException ice) { - // the parsed JWT had a sub field, but its value was not equal to 'jsmith' -} -``` - -You can also require custom fields by using the `require(fieldName, requiredFieldValue)` method - for example: - -```java -try { - Jwts.parser().require("myfield", "myRequiredValue").setSigningKey(key).parseClaimsJws(s); -} catch(InvalidClaimException ice) { - // the 'myfield' field was missing or did not have a 'myRequiredValue' value -} -``` -(or, again, you could catch either MissingClaimException or IncorrectClaimException instead) - -#### Body Compression - -**This feature is NOT JWT specification compliant**, *but it can be very useful when you parse your own tokens*. - -If your JWT body is large and you have size restrictions (for example, if embedding a JWT in a URL and the URL must be under a certain length for legacy browsers or mail user agents), you may now compress the JWT body using a `CompressionCodec`: - -```java -Jwts.builder().claim("foo", "someReallyLongDataString...") - .compressWith(CompressionCodecs.DEFLATE) // or CompressionCodecs.GZIP - .signWith(SignatureAlgorithm.HS256, key) - .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. - -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: - -```java -Jwts.parser().setSigningKey(key).parseClaimsJws(compact); -``` - -##### Custom Compression Algorithms - -If the DEFLATE or GZIP algorithms are not sufficient for your needs, you can specify your own Compression algorithms by implementing the `CompressionCodec` interface and setting it on the parser: - -```java -Jwts.builder().claim("foo", "someReallyLongDataString...") - .compressWith(new MyCompressionCodec()) - .signWith(SignatureAlgorithm.HS256, key) - .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: - -```java -Jwts.parser().setSigningKey(key) - .setCompressionCodecResolver(new MyCustomCompressionCodecResolver()) - .parseClaimsJws(compact); -``` - -*NOTE*: Because body compression is not JWT specification compliant, you should only enable compression if both your JWT builder and parser are JJWT versions >= 0.6.0, or if you're using another library that implements the exact same functionality. This feature is best reserved for your own use cases - where you both create and later parse the tokens. It will likely cause problems if you compressed a token and expected a 3rd party (who doesn't use JJWT) to parse the token. - -### 0.5.1 - -- Minor [bug](https://github.com/jwtk/jjwt/issues/31) fix [release](https://github.com/jwtk/jjwt/issues?q=milestone%3A0.5.1+is%3Aclosed) that ensures correct Base64 padding in Android runtimes. - -### 0.5 - -- Android support! Android's built-in Base64 codec will be used if JJWT detects it is running in an Android environment. Other than Base64, all other parts of JJWT were already Android-compliant. Now it is fully compliant. - -- Elliptic Curve signature algorithms! `SignatureAlgorithm.ES256`, `ES384` and `ES512` are now supported. - -- Super convenient key generation methods, so you don't have to worry how to do this safely: - - `MacProvider.generateKey(); //or generateKey(SignatureAlgorithm)` - - `RsaProvider.generateKeyPair(); //or generateKeyPair(sizeInBits)` - - `EllipticCurveProvider.generateKeyPair(); //or generateKeyPair(SignatureAlgorithm)` - - The `generate`* methods that accept an `SignatureAlgorithm` argument know to generate a key of sufficient strength that reflects the specified algorithm strength. - -Please see the full [0.5 closed issues list](https://github.com/jwtk/jjwt/issues?q=milestone%3A0.5+is%3Aclosed) for more information. - -### 0.4 - -- [Issue 8](https://github.com/jwtk/jjwt/issues/8): Add ability to find signing key by inspecting the JWS values before verifying the signature. - -This is a handy little feature. If you need to parse a signed JWT (a JWS) and you don't know which signing key was used to sign it, you can now use the new `SigningKeyResolver` concept. - -A `SigningKeyresolver` can inspect the JWS header and body (Claims or String) _before_ the JWS signature is verified. By inspecting the data, you can find the key and return it, and the parser will use the returned key to validate the signature. For example: - -```java -SigningKeyResolver resolver = new MySigningKeyResolver(); - -Jws jws = Jwts.parser().setSigningKeyResolver(resolver).parseClaimsJws(compact); -``` - -The signature is still validated, and the JWT instance will still not be returned if the jwt string is invalid, as expected. You just get to 'see' the JWT data for key discovery before the parser validates. Nice. - -This of course requires that you put some sort of information in the JWS when you create it so that your `SigningKeyResolver` implementation can look at it later and look up the key. The *standard* way to do this is to use the JWS `kid` ('key id') field, for example: - -```java -Jwts.builder().setHeaderParam("kid", your_signing_key_id_NOT_THE_SECRET).build(); -``` - -You could of course set any other header parameter or claims parameter instead of setting `kid` if you want - that's just the default field reserved for signing key identification. If you can locate the signing key based on other information in the header or claims, you don't need to set the `kid` field - just make sure your resolver implementation knows how to look up the key. - -Finally, a nice `SigningKeyResolverAdapter` is provided to allow you to write quick and simple subclasses or anonymous classes instead of having to implement the `SigningKeyResolver` interface directly. For example: - -```java -Jws jws = Jwts.parser().setSigningKeyResolver(new SigningKeyResolverAdapter() { - @Override - public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { - //inspect the header or claims, lookup and return the signing key - String keyId = header.getKeyId(); //or any other field that you need to inspect - return getSigningKey(keyId); //implement me - }}) - .parseClaimsJws(compact); -``` - -### 0.3 - -- [Issue 6](https://github.com/jwtk/jjwt/issues/6): Parsing an expired Claims JWT or JWS (as determined by the `exp` claims field) will now throw an `ExpiredJwtException`. -- [Issue 7](https://github.com/jwtk/jjwt/issues/7): Parsing a premature Claims JWT or JWS (as determined by the `nbf` claims field) will now throw a `PrematureJwtException`. - -### 0.2 - -#### More convenient Claims building - -This release adds convenience methods to the `JwtBuilder` interface so you can set claims directly on the builder without having to create a separate Claims instance/builder, reducing the amount of code you have to write. For example, this: - -```java -Claims claims = Jwts.claims().setSubject("Joe"); - -String compactJwt = Jwts.builder().setClaims(claims).signWith(HS256, key).compact(); -``` - -can now be written as: - -```java -String compactJwt = Jwts.builder().setSubject("Joe").signWith(HS256, key).compact(); -``` - -A Claims instance based on the specified claims will be created and set as the JWT's payload automatically. - -#### Type-safe handling for JWT and JWS with generics - -The following < 0.2 code produced a JWT as expected: - -```java -Jwt jwt = Jwts.parser().setSigningKey(key).parse(compact); -``` - -But you couldn't easily determine if the `jwt` was a `JWT` or `JWS` instance or if the body was a `Claims` instance or a plaintext `String` without resorting to a bunch of yucky `instanceof` checks. In 0.2, we introduce the `JwtHandler` when you don't know the exact format of the compact JWT string ahead of time, and parsing convenience methods when you do. - -##### JwtHandler - -If you do not know the format of the compact JWT string at the time you try to parse it, you can determine what type it is after parsing by providing a `JwtHandler` instance to the `JwtParser` with the new `parse(String compactJwt, JwtHandler handler)` method. For example: - -```java -T returnVal = Jwts.parser().setSigningKey(key).parse(compact, new JwtHandler() { - @Override - public T onPlaintextJwt(Jwt jwt) { - //the JWT parsed was an unsigned plaintext JWT - //inspect it, then return an instance of T (see returnVal above) - } - - @Override - public T onClaimsJwt(Jwt jwt) { - //the JWT parsed was an unsigned Claims JWT - //inspect it, then return an instance of T (see returnVal above) - } - - @Override - public T onPlaintextJws(Jws jws) { - //the JWT parsed was a signed plaintext JWS - //inspect it, then return an instance of T (see returnVal above) - } - - @Override - public T onClaimsJws(Jws jws) { - //the JWT parsed was a signed Claims JWS - //inspect it, then return an instance of T (see returnVal above) - } -}); -``` - -Of course, if you know you'll only have to parse a subset of the above, you can use the `JwtHandlerAdapter` and implement only the methods you need. For example: - -```java -T returnVal = Jwts.parser().setSigningKey(key).parse(plaintextJwt, new JwtHandlerAdapter>() { - @Override - public T onPlaintextJws(Jws jws) { - //the JWT parsed was a signed plaintext JWS - //inspect it, then return an instance of T (see returnVal above) - } - - @Override - public T onClaimsJws(Jws jws) { - //the JWT parsed was a signed Claims JWS - //inspect it, then return an instance of T (see returnVal above) - } -}); -``` - -##### Known Type convenience parse methods - -If, unlike above, you are confident of the compact string format and know which type of JWT or JWS it will produce, you can just use one of the 4 new convenience parsing methods to get exactly the type of JWT or JWS you know exists. For example: - -```java - -//for a known plaintext jwt string: -Jwt jwt = Jwts.parser().parsePlaintextJwt(compact); - -//for a known Claims JWT string: -Jwt jwt = Jwts.parser().parseClaimsJwt(compact); - -//for a known signed plaintext JWT (aka a plaintext JWS): -Jws jws = Jwts.parser().setSigningKey(key).parsePlaintextJws(compact); - -//for a known signed Claims JWT (aka a Claims JWS): -Jws jws = Jwts.parser().setSigningKey(key).parseClaimsJws(compact); - -``` +- [JJWT Changelog](CHANGELOG.md) #### Already using an older Jackson dependency? -JJWT depends on Jackson 2.4.x (or later). If you are already using a Jackson version in your own application less than 2.x, for example 1.9.x, you will likely see [runtime errors](https://github.com/jwtk/jjwt/issues/1). To avoid this, you should change your project build configuration to explicitly point to a 2.x version of Jackson. For example: +JJWT depends on Jackson 2.8.x (or later). If you are already using a Jackson version in your own application less than 2.x, for example 1.9.x, you will likely see [runtime errors](https://github.com/jwtk/jjwt/issues/1). To avoid this, you should change your project build configuration to explicitly point to a 2.x version of Jackson. For example: ```xml com.fasterxml.jackson.core jackson-databind - 2.4.2 + 2.8.2 ``` @@ -370,4 +235,4 @@ Maintained by [Stormpath](https://stormpath.com/) ## Licensing -This project is open-source via the [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0). \ No newline at end of file +This project is open-source via the [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0). diff --git a/pom.xml b/pom.xml index 85b5a3c3..f62505fa 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ io.jsonwebtoken jjwt - 0.7.0-SNAPSHOT + 0.8.0-SNAPSHOT JSON Web Token support for the JVM jar @@ -54,25 +54,25 @@ - 2.4 - 3.1 + 3.0.2 + 3.5.1 1.6 UTF-8 ${user.name}-${maven.build.timestamp} - 2.7.0 + 2.8.2 - 1.51 + 1.55 - 2.3.0-beta-2 - 1.0.7 - 3.3.1 + 2.4.7 + 1.1.7 + 3.4 4.12 - 1.6.2 - 2.12.4 + 1.6.5 + 2.19.1 @@ -270,79 +270,19 @@ - org.codehaus.mojo - cobertura-maven-plugin - 2.7 + org.jacoco + jacoco-maven-plugin + 0.7.6.201602180812 - 256m - true - - - io.jsonwebtoken.lang.* - - - - 100 - 100 - - true - - - - io.jsonwebtoken.impl.serialization.* - 0 - 0 - - - - io.jsonwebtoken.impl.DefaultJweFactory - 0 - 0 - - - - io.jsonwebtoken.impl.DefaultJweHeader - 0 - 0 - - - - io.jsonwebtoken.impl.DispatchingParser - 0 - 0 - - - - io.jsonwebtoken.lang.* - 0 - 0 - - - - io.jsonwebtoken.impl.DefaultClaims - 97 - 100 - - - io.jsonwebtoken.impl.DefaultJwtParser - 100 - 90 - - - - - xml - html - + + **/io/jsonwebtoken/lang/* + + prepare-agent - clean - check + prepare-agent @@ -398,7 +338,6 @@ - jdk8 @@ -465,5 +404,4 @@ - diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index e0373093..fd1f6c7e 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -131,11 +131,21 @@ public interface JwtParser { * The parser uses a {@link DefaultClock DefaultClock} instance by default. * * @param clock a {@code Clock} object to return the timestamp to use when validating the parsed JWT. - * @return the builder instance for method chaining. + * @return the parser for method chaining. * @since 0.7.0 */ JwtParser setClock(Clock clock); + /** + * Sets the amount of clock skew in seconds to tolerate when verifying the local time against the {@code exp} + * and {@code nbf} claims. + * + * @param seconds the number of seconds to tolerate for clock skew when verifying {@code exp} or {@code nbf} claims. + * @return the parser for method chaining. + * @since 0.7.0 + */ + JwtParser setAllowedClockSkewSeconds(long seconds); + /** * 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. diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java b/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java index 196c82ff..2523d305 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java @@ -120,10 +120,25 @@ public class DefaultClaims extends JwtMap implements Claims { value = getDate(claimName); } + return castClaimValue(value, requiredType); + } + + private T castClaimValue(Object value, Class requiredType) { if (requiredType == Date.class && value instanceof Long) { value = new Date((Long)value); } + if (value instanceof Integer) { + int intValue = (Integer) value; + if (requiredType == Long.class) { + value = (long) intValue; + } else if (requiredType == Short.class && Short.MIN_VALUE <= intValue && intValue <= Short.MAX_VALUE) { + value = (short) intValue; + } else if (requiredType == Byte.class && Byte.MIN_VALUE <= intValue && intValue <= Byte.MAX_VALUE) { + value = (byte) intValue; + } + } + if (!requiredType.isInstance(value)) { throw new RequiredTypeException("Expected value to be of type: " + requiredType + ", but was " + value.getClass()); } diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index f8d3204a..4e4b9c79 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -56,7 +56,8 @@ import java.util.Map; public class DefaultJwtParser implements JwtParser { //don't need millis since JWT date fields are only second granularity: - private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ"; + private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + private static final int MILLISECONDS_PER_SECOND = 1000; private ObjectMapper objectMapper = new ObjectMapper(); @@ -72,52 +73,47 @@ public class DefaultJwtParser implements JwtParser { private Clock clock = DefaultClock.INSTANCE; + private long allowedClockSkewMillis = 0; + @Override public JwtParser requireIssuedAt(Date issuedAt) { expectedClaims.setIssuedAt(issuedAt); - return this; } @Override public JwtParser requireIssuer(String issuer) { expectedClaims.setIssuer(issuer); - return this; } @Override public JwtParser requireAudience(String audience) { expectedClaims.setAudience(audience); - return this; } @Override public JwtParser requireSubject(String subject) { expectedClaims.setSubject(subject); - return this; } @Override public JwtParser requireId(String id) { expectedClaims.setId(id); - return this; } @Override public JwtParser requireExpiration(Date expiration) { expectedClaims.setExpiration(expiration); - return this; } @Override public JwtParser requireNotBefore(Date notBefore) { expectedClaims.setNotBefore(notBefore); - return this; } @@ -126,7 +122,6 @@ public class DefaultJwtParser implements JwtParser { Assert.hasText(claimName, "claim name cannot be null or empty."); Assert.notNull(value, "The value cannot be null for claim name: " + claimName); expectedClaims.put(claimName, value); - return this; } @@ -137,6 +132,12 @@ public class DefaultJwtParser implements JwtParser { return this; } + @Override + public JwtParser setAllowedClockSkewSeconds(long seconds) { + this.allowedClockSkewMillis = Math.max(0, seconds * MILLISECONDS_PER_SECOND); + return this; + } + @Override public JwtParser setSigningKey(byte[] key) { Assert.notEmpty(key, "signing key cannot be null or empty."); @@ -213,7 +214,8 @@ public class DefaultJwtParser implements JwtParser { if (c == SEPARATOR_CHAR) { - String token = Strings.clean(sb.toString()); + CharSequence tokenSeq = Strings.clean(sb); + String token = tokenSeq!=null?tokenSeq.toString():null; if (delimiterCount == 0) { base64UrlEncodedHeader = token; @@ -222,7 +224,7 @@ public class DefaultJwtParser implements JwtParser { } delimiterCount++; - sb = new StringBuilder(128); + sb.setLength(0); } else { sb.append(c); } @@ -319,8 +321,8 @@ public class DefaultJwtParser implements JwtParser { if (!Objects.isEmpty(keyBytes)) { - Assert.isTrue(!algorithm.isRsa(), - "Key bytes cannot be specified for RSA signatures. Please specify a PublicKey or PrivateKey instance."); + Assert.isTrue(algorithm.isHmac(), + "Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance."); key = new SecretKeySpec(keyBytes, algorithm.getJcaName()); } @@ -353,24 +355,33 @@ public class DefaultJwtParser implements JwtParser { } } + final boolean allowSkew = this.allowedClockSkewMillis > 0; + //since 0.3: if (claims != null) { SimpleDateFormat sdf; final Date now = this.clock.now(); + long nowTime = now.getTime(); //https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-30#section-4.1.4 //token MUST NOT be accepted on or after any specified exp time: Date exp = claims.getExpiration(); if (exp != null) { - if (now.equals(exp) || now.after(exp)) { + long maxTime = nowTime - this.allowedClockSkewMillis; + Date max = allowSkew ? new Date(maxTime) : now; + if (max.after(exp)) { sdf = new SimpleDateFormat(ISO_8601_FORMAT); String expVal = sdf.format(exp); String nowVal = sdf.format(now); - String msg = "JWT expired at " + expVal + ". Current time: " + nowVal; + long differenceMillis = maxTime - exp.getTime(); + + String msg = "JWT expired at " + expVal + ". Current time: " + nowVal + ", a difference of " + + differenceMillis + " milliseconds. Allowed clock skew: " + + this.allowedClockSkewMillis + " milliseconds."; throw new ExpiredJwtException(header, claims, msg); } } @@ -380,12 +391,19 @@ public class DefaultJwtParser implements JwtParser { Date nbf = claims.getNotBefore(); if (nbf != null) { - if (now.before(nbf)) { + long minTime = nowTime + this.allowedClockSkewMillis; + Date min = allowSkew ? new Date(minTime) : now; + if (min.before(nbf)) { sdf = new SimpleDateFormat(ISO_8601_FORMAT); String nbfVal = sdf.format(nbf); String nowVal = sdf.format(now); - String msg = "JWT must not be accepted before " + nbfVal + ". Current time: " + nowVal; + long differenceMillis = nbf.getTime() - minTime; + + String msg = "JWT must not be accepted before " + nbfVal + ". Current time: " + nowVal + + ", a difference of " + + differenceMillis + " milliseconds. Allowed clock skew: " + + this.allowedClockSkewMillis + " milliseconds."; throw new PrematureJwtException(header, claims, msg); } } diff --git a/src/main/java/io/jsonwebtoken/impl/JwtMap.java b/src/main/java/io/jsonwebtoken/impl/JwtMap.java index 40d21224..2c3329cb 100644 --- a/src/main/java/io/jsonwebtoken/impl/JwtMap.java +++ b/src/main/java/io/jsonwebtoken/impl/JwtMap.java @@ -47,10 +47,16 @@ public class JwtMap implements Map { } else if (v instanceof Date) { return (Date) v; } else if (v instanceof Number) { + // https://github.com/jwtk/jjwt/issues/122: + // The JWT RFC *mandates* NumericDate values are represented as seconds. + // Because Because java.util.Date requires milliseconds, we need to multiply by 1000: long seconds = ((Number) v).longValue(); long millis = seconds * 1000; return new Date(millis); } else if (v instanceof String) { + // https://github.com/jwtk/jjwt/issues/122 + // The JWT RFC *mandates* NumericDate values are represented as seconds. + // Because Because java.util.Date requires milliseconds, we need to multiply by 1000: long seconds = Long.parseLong((String) v); long millis = seconds * 1000; return new Date(millis); @@ -155,4 +161,14 @@ public class JwtMap implements Map { public String toString() { return map.toString(); } + + @Override + public int hashCode() { + return map.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return map.equals(obj); + } } diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java index 1c215e22..f388c8d9 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java @@ -15,9 +15,6 @@ */ package io.jsonwebtoken.impl.crypto; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.lang.Assert; - import java.security.Key; import java.security.KeyPair; import java.security.KeyPairGenerator; @@ -25,6 +22,10 @@ import java.security.SecureRandom; import java.util.HashMap; import java.util.Map; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.lang.Assert; + /** * ElliptiCurve crypto provider. * @@ -135,4 +136,166 @@ public abstract class EllipticCurveProvider extends SignatureProvider { throw new IllegalStateException("Unable to generate Elliptic Curve KeyPair: " + e.getMessage(), e); } } + + /** + * Returns the expected signature byte array length (R + S parts) for + * the specified ECDSA algorithm. + * + * @param alg The ECDSA algorithm. Must be supported and not + * {@code null}. + * + * @return The expected byte array length for the signature. + * + * @throws JwtException If the algorithm is not supported. + */ + public static int getSignatureByteArrayLength(final SignatureAlgorithm alg) + throws JwtException { + + switch (alg) { + case ES256: return 64; + case ES384: return 96; + case ES512: return 132; + default: + throw new JwtException("Unsupported Algorithm: " + alg.name()); + } + } + + + /** + * Transcodes the JCA ASN.1/DER-encoded signature into the concatenated + * R + S format expected by ECDSA JWS. + * + * @param derSignature The ASN1./DER-encoded. Must not be {@code null}. + * @param outputLength The expected length of the ECDSA JWS signature. + * + * @return The ECDSA JWS encoded signature. + * + * @throws JwtException If the ASN.1/DER signature format is invalid. + */ + public static byte[] transcodeSignatureToConcat(final byte[] derSignature, int outputLength) + throws JwtException { + + if (derSignature.length < 8 || derSignature[0] != 48) { + throw new JwtException("Invalid ECDSA signature format"); + } + + int offset; + if (derSignature[1] > 0) { + offset = 2; + } else if (derSignature[1] == (byte) 0x81) { + offset = 3; + } else { + throw new JwtException("Invalid ECDSA signature format"); + } + + byte rLength = derSignature[offset + 1]; + + int i = rLength; + while ((i > 0) + && (derSignature[(offset + 2 + rLength) - i] == 0)) + i--; + + byte sLength = derSignature[offset + 2 + rLength + 1]; + + int j = sLength; + while ((j > 0) + && (derSignature[(offset + 2 + rLength + 2 + sLength) - j] == 0)) + j--; + + int rawLen = Math.max(i, j); + rawLen = Math.max(rawLen, outputLength / 2); + + if ((derSignature[offset - 1] & 0xff) != derSignature.length - offset + || (derSignature[offset - 1] & 0xff) != 2 + rLength + 2 + sLength + || derSignature[offset] != 2 + || derSignature[offset + 2 + rLength] != 2) { + throw new JwtException("Invalid ECDSA signature format"); + } + + final byte[] concatSignature = new byte[2 * rawLen]; + + System.arraycopy(derSignature, (offset + 2 + rLength) - i, concatSignature, rawLen - i, i); + System.arraycopy(derSignature, (offset + 2 + rLength + 2 + sLength) - j, concatSignature, 2 * rawLen - j, j); + + return concatSignature; + } + + + + /** + * Transcodes the ECDSA JWS signature into ASN.1/DER format for use by + * the JCA verifier. + * + * @param jwsSignature The JWS signature, consisting of the + * concatenated R and S values. Must not be + * {@code null}. + * + * @return The ASN.1/DER encoded signature. + * + * @throws JwtException If the ECDSA JWS signature format is invalid. + */ + public static byte[] transcodeSignatureToDER(byte[] jwsSignature) + throws JwtException { + + int rawLen = jwsSignature.length / 2; + + int i = rawLen; + + while((i > 0) + && (jwsSignature[rawLen - i] == 0)) + i--; + + int j = i; + + if (jwsSignature[rawLen - i] < 0) { + j += 1; + } + + int k = rawLen; + + while ((k > 0) + && (jwsSignature[2 * rawLen - k] == 0)) + k--; + + int l = k; + + if (jwsSignature[2 * rawLen - k] < 0) { + l += 1; + } + + int len = 2 + j + 2 + l; + + if (len > 255) { + throw new JwtException("Invalid ECDSA signature format"); + } + + int offset; + + final byte derSignature[]; + + if (len < 128) { + derSignature = new byte[2 + 2 + j + 2 + l]; + offset = 1; + } else { + derSignature = new byte[3 + 2 + j + 2 + l]; + derSignature[1] = (byte) 0x81; + offset = 2; + } + + derSignature[0] = 48; + derSignature[offset++] = (byte) len; + derSignature[offset++] = 2; + derSignature[offset++] = (byte) j; + + System.arraycopy(jwsSignature, rawLen - i, derSignature, (offset + j) - i, i); + + offset += j; + + derSignature[offset++] = 2; + derSignature[offset++] = (byte) l; + + System.arraycopy(jwsSignature, 2 * rawLen - k, derSignature, (offset + l) - k, k); + + return derSignature; + } } diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java index 62097184..09ab14db 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java @@ -15,16 +15,16 @@ */ package io.jsonwebtoken.impl.crypto; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.SignatureException; -import io.jsonwebtoken.lang.Assert; - import java.security.InvalidKeyException; import java.security.Key; import java.security.PublicKey; import java.security.Signature; import java.security.interfaces.ECPublicKey; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.SignatureException; +import io.jsonwebtoken.lang.Assert; + public class EllipticCurveSignatureValidator extends EllipticCurveProvider implements SignatureValidator { private static final String EC_PUBLIC_KEY_REQD_MSG = @@ -40,7 +40,16 @@ public class EllipticCurveSignatureValidator extends EllipticCurveProvider imple Signature sig = createSignatureInstance(); PublicKey publicKey = (PublicKey) key; try { - return doVerify(sig, publicKey, data, signature); + int expectedSize = getSignatureByteArrayLength(alg); + /** + * + * If the expected size is not valid for JOSE, fall back to ASN.1 DER signature. + * This fallback is for backwards compatibility ONLY (to support tokens generated by previous versions of jjwt) + * and backwards compatibility will possibly be removed in a future version of this library. + * + * **/ + byte[] derSignature = expectedSize != signature.length && signature[0] == 0x30 ? signature : EllipticCurveProvider.transcodeSignatureToDER(signature); + return doVerify(sig, publicKey, data, derSignature); } catch (Exception e) { String msg = "Unable to verify Elliptic Curve signature using configured ECPublicKey. " + e.getMessage(); throw new SignatureException(msg, e); diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java index 4c80268f..14913f18 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java @@ -15,15 +15,16 @@ */ package io.jsonwebtoken.impl.crypto; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.SignatureException; - import java.security.InvalidKeyException; import java.security.Key; import java.security.PrivateKey; import java.security.Signature; import java.security.interfaces.ECPrivateKey; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.SignatureException; + public class EllipticCurveSigner extends EllipticCurveProvider implements Signer { public EllipticCurveSigner(SignatureAlgorithm alg, Key key) { @@ -43,14 +44,16 @@ public class EllipticCurveSigner extends EllipticCurveProvider implements Signer throw new SignatureException("Invalid Elliptic Curve PrivateKey. " + e.getMessage(), e); } catch (java.security.SignatureException e) { throw new SignatureException("Unable to calculate signature using Elliptic Curve PrivateKey. " + e.getMessage(), e); + } catch (JwtException e) { + throw new SignatureException("Unable to convert signature to JOSE format. " + e.getMessage(), e); } } - protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException { + protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException, JwtException { PrivateKey privateKey = (PrivateKey)key; Signature sig = createSignatureInstance(); sig.initSign(privateKey); sig.update(data); - return sig.sign(); + return transcodeSignatureToConcat(sig.sign(), getSignatureByteArrayLength(alg)); } } diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java b/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java index 7ea2ef9c..1ce280bb 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java @@ -69,7 +69,7 @@ public abstract class MacProvider extends SignatureProvider { * * * - *
Signature Algorithm Generated Key Size
HS256 256 bits (32 bytes)
HS384 384 bits (48 bytes)
HS512 256 bits (64 bytes)
+ * HS512 512 bits (64 bytes) * * @param alg the signature algorithm that will be used with the generated key * @param random the secure random number generator used during key generation diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java b/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java index 4fb4f8a0..e10c7a77 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java @@ -15,16 +15,16 @@ */ package io.jsonwebtoken.impl.crypto; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.SignatureException; -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.RuntimeEnvironment; - import java.security.Key; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.Signature; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.SignatureException; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.RuntimeEnvironment; + abstract class SignatureProvider { /** diff --git a/src/main/java/io/jsonwebtoken/lang/Strings.java b/src/main/java/io/jsonwebtoken/lang/Strings.java index d1974260..a93cf340 100644 --- a/src/main/java/io/jsonwebtoken/lang/Strings.java +++ b/src/main/java/io/jsonwebtoken/lang/Strings.java @@ -159,22 +159,38 @@ public final class Strings { * @see java.lang.Character#isWhitespace */ public static String trimWhitespace(String str) { + return (String) trimWhitespace((CharSequence)str); + } + + + private static CharSequence trimWhitespace(CharSequence str) { if (!hasLength(str)) { return str; } - StringBuilder sb = new StringBuilder(str); - while (sb.length() > 0 && Character.isWhitespace(sb.charAt(0))) { - sb.deleteCharAt(0); + final int length = str.length(); + + int start = 0; + while (start < length && Character.isWhitespace(str.charAt(start))) { + start++; } - while (sb.length() > 0 && Character.isWhitespace(sb.charAt(sb.length() - 1))) { - sb.deleteCharAt(sb.length() - 1); + + int end = length; + while (start < length && Character.isWhitespace(str.charAt(end - 1))) { + end--; } - return sb.toString(); + + return ((start > 0) || (end < length)) ? str.subSequence(start, end) : str; } public static String clean(String str) { + CharSequence result = clean((CharSequence) str); + + return result!=null?result.toString():null; + } + + public static CharSequence clean(CharSequence str) { str = trimWhitespace(str); - if ("".equals(str)) { + if (!hasLength(str)) { return null; } return str; diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 40dba0c7..187711fe 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -175,6 +175,9 @@ class JwtParserTest { fail() } catch (ExpiredJwtException e) { assertTrue e.getMessage().startsWith('JWT expired at ') + + //https://github.com/jwtk/jjwt/issues/107 (the Z designator at the end of the timestamp): + assertTrue e.getMessage().contains('Z, a difference of ') } } @@ -188,6 +191,61 @@ class JwtParserTest { try { Jwts.parser().parse(compact) fail() + } catch (PrematureJwtException e) { + assertTrue e.getMessage().startsWith('JWT must not be accepted before ') + + //https://github.com/jwtk/jjwt/issues/107 (the Z designator at the end of the timestamp): + assertTrue e.getMessage().contains('Z, a difference of ') + } + } + + @Test + void testParseWithExpiredJwtWithinAllowedClockSkew() { + Date exp = new Date(System.currentTimeMillis() - 3000) + + String subject = 'Joe' + String compact = Jwts.builder().setSubject(subject).setExpiration(exp).compact() + + Jwt jwt = Jwts.parser().setAllowedClockSkewSeconds(10).parse(compact) + + assertEquals jwt.getBody().getSubject(), subject + } + + @Test + void testParseWithExpiredJwtNotWithinAllowedClockSkew() { + Date exp = new Date(System.currentTimeMillis() - 3000) + + String compact = Jwts.builder().setSubject('Joe').setExpiration(exp).compact() + + try { + Jwts.parser().setAllowedClockSkewSeconds(1).parse(compact) + fail() + } catch (ExpiredJwtException e) { + assertTrue e.getMessage().startsWith('JWT expired at ') + } + } + + @Test + void testParseWithPrematureJwtWithinAllowedClockSkew() { + Date exp = new Date(System.currentTimeMillis() + 3000) + + String subject = 'Joe' + String compact = Jwts.builder().setSubject(subject).setNotBefore(exp).compact() + + Jwt jwt = Jwts.parser().setAllowedClockSkewSeconds(10).parse(compact) + + assertEquals jwt.getBody().getSubject(), subject + } + + @Test + void testParseWithPrematureJwtNotWithinAllowedClockSkew() { + Date exp = new Date(System.currentTimeMillis() + 3000) + + String compact = Jwts.builder().setSubject('Joe').setNotBefore(exp).compact() + + try { + Jwts.parser().setAllowedClockSkewSeconds(1).parse(compact) + fail() } catch (PrematureJwtException e) { assertTrue e.getMessage().startsWith('JWT must not be accepted before ') } @@ -658,6 +716,37 @@ class JwtParserTest { } } + @Test + void testParseClaimsJwsWithNumericTypes() { + byte[] key = randomKey() + + def b = (byte) 42 + def s = (short) 42 + def i = 42 + + def smallLong = (long) 42 + def bigLong = ((long) Integer.MAX_VALUE) + 42 + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + claim("byte", b). + claim("short", s). + claim("int", i). + claim("long_small", smallLong). + claim("long_big", bigLong). + compact() + + Jwt jwt = Jwts.parser().setSigningKey(key).parseClaimsJws(compact) + + Claims claims = jwt.getBody() + + assertEquals(b, claims.get("byte", Byte.class)) + assertEquals(s, claims.get("short", Short.class)) + assertEquals(i, claims.get("int", Integer.class)) + assertEquals(smallLong, claims.get("long_small", Long.class)) + assertEquals(bigLong, claims.get("long_big", Long.class)) + } + + // ======================================================================== // parsePlaintextJws with signingKey resolver. // ======================================================================== @@ -888,7 +977,7 @@ class JwtParserTest { // system converts to seconds (lopping off millis precision), then returns millis def issuedAtMillis = ((long)issuedAt.getTime() / 1000) * 1000 - assertEquals jwt.getBody().getIssuedAt().getTime(), issuedAtMillis + assertEquals jwt.getBody().getIssuedAt().getTime(), issuedAtMillis, 0 } @Test @@ -1212,7 +1301,7 @@ class JwtParserTest { // system converts to seconds (lopping off millis precision), then returns millis def expirationMillis = ((long)expiration.getTime() / 1000) * 1000 - assertEquals jwt.getBody().getExpiration().getTime(), expirationMillis + assertEquals jwt.getBody().getExpiration().getTime(), expirationMillis, 0 } @Test @@ -1280,7 +1369,7 @@ class JwtParserTest { // system converts to seconds (lopping off millis precision), then returns millis def notBeforeMillis = ((long)notBefore.getTime() / 1000) * 1000 - assertEquals jwt.getBody().getNotBefore().getTime(), notBeforeMillis + assertEquals jwt.getBody().getNotBefore().getTime(), notBeforeMillis, 0 } @Test diff --git a/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy index 161957f0..4d642322 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy @@ -52,11 +52,103 @@ class DefaultClaimsTest { } @Test - void testGetClaimWithRequiredType_Success() { - claims.put("anInteger", new Integer(5)) + void testGetClaimWithRequiredType_Integer_Success() { + def expected = new Integer(5) + claims.put("anInteger", expected) Object result = claims.get("anInteger", Integer.class) + assertEquals(expected, result) + } - assertTrue(result instanceof Integer) + @Test + void testGetClaimWithRequiredType_Long_Success() { + def expected = new Long(123) + claims.put("aLong", expected) + Object result = claims.get("aLong", Long.class) + assertEquals(expected, result) + } + + @Test + void testGetClaimWithRequiredType_LongWithInteger_Success() { + // long value that fits inside an Integer + def expected = new Long(Integer.MAX_VALUE - 100) + // deserialized as an Integer from JSON + // (type information is not available during parsing) + claims.put("smallLong", expected.intValue()) + // should still be available as Long + Object result = claims.get("smallLong", Long.class) + assertEquals(expected, result) + } + + @Test + void testGetClaimWithRequiredType_ShortWithInteger_Success() { + def expected = new Short((short) 42) + claims.put("short", expected.intValue()) + Object result = claims.get("short", Short.class) + assertEquals(expected, result) + } + + @Test + void testGetClaimWithRequiredType_ShortWithBigInteger_Exception() { + claims.put("tooBigForShort", ((int) Short.MAX_VALUE) + 42) + try { + claims.get("tooBigForShort", Short.class) + fail("getClaim() shouldn't silently lose precision.") + } catch (RequiredTypeException e) { + assertEquals( + e.getMessage(), + "Expected value to be of type: class java.lang.Short, but was class java.lang.Integer" + ) + } + } + + @Test + void testGetClaimWithRequiredType_ShortWithSmallInteger_Exception() { + claims.put("tooSmallForShort", ((int) Short.MIN_VALUE) - 42) + try { + claims.get("tooSmallForShort", Short.class) + fail("getClaim() shouldn't silently lose precision.") + } catch (RequiredTypeException e) { + assertEquals( + e.getMessage(), + "Expected value to be of type: class java.lang.Short, but was class java.lang.Integer" + ) + } + } + + @Test + void testGetClaimWithRequiredType_ByteWithInteger_Success() { + def expected = new Byte((byte) 42) + claims.put("byte", expected.intValue()) + Object result = claims.get("byte", Byte.class) + assertEquals(expected, result) + } + + @Test + void testGetClaimWithRequiredType_ByteWithBigInteger_Exception() { + claims.put("tooBigForByte", ((int) Byte.MAX_VALUE) + 42) + try { + claims.get("tooBigForByte", Byte.class) + fail("getClaim() shouldn't silently lose precision.") + } catch (RequiredTypeException e) { + assertEquals( + e.getMessage(), + "Expected value to be of type: class java.lang.Byte, but was class java.lang.Integer" + ) + } + } + + @Test + void testGetClaimWithRequiredType_ByteWithSmallInteger_Exception() { + claims.put("tooSmallForByte", ((int) Byte.MIN_VALUE) - 42) + try { + claims.get("tooSmallForByte", Byte.class) + fail("getClaim() shouldn't silently lose precision.") + } catch (RequiredTypeException e) { + assertEquals( + e.getMessage(), + "Expected value to be of type: class java.lang.Byte, but was class java.lang.Integer" + ) + } } @Test diff --git a/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy index 0d00f6d7..446eeddd 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy @@ -124,4 +124,28 @@ class JwtMapTest { def s = ['b', 'd'] assertTrue m.values().containsAll(s) && s.containsAll(m.values()) } + + @Test + public void testEquals() throws Exception { + def m1 = new JwtMap(); + m1.put("a", "a"); + + def m2 = new JwtMap(); + m2.put("a", "a"); + + assertEquals(m1, m2); + } + + @Test + public void testHashcode() throws Exception { + def m = new JwtMap(); + def hashCodeEmpty = m.hashCode(); + + m.put("a", "b"); + def hashCodeNonEmpty = m.hashCode(); + assertTrue(hashCodeEmpty != hashCodeNonEmpty); + + def identityHash = System.identityHashCode(m); + assertTrue(hashCodeNonEmpty != identityHash); + } } diff --git a/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy index e595bd89..f23c3852 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy @@ -15,18 +15,24 @@ */ package io.jsonwebtoken.impl.crypto +import io.jsonwebtoken.JwtException import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.SignatureException - -import java.security.InvalidKeyException -import java.security.PublicKey -import java.security.Signature - +import io.jsonwebtoken.impl.TextCodec +import org.bouncycastle.jce.provider.BouncyCastleProvider import org.junit.Test + +import java.security.* +import java.security.spec.X509EncodedKeySpec + import static org.junit.Assert.* class EllipticCurveSignatureValidatorTest { + static { + Security.addProvider(new BouncyCastleProvider()) + } + @Test void testDoVerifyWithInvalidKeyException() { @@ -53,4 +59,146 @@ class EllipticCurveSignatureValidatorTest { assertSame se.cause, ex } } + + @Test + void ecdsaSignatureComplianceTest() { + def fact = KeyFactory.getInstance("ECDSA", "BC"); + def publicKey = "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQASisgweVL1tAtIvfmpoqvdXF8sPKTV9YTKNxBwkdkm+/auh4pR8TbaIfsEzcsGUVv61DFNFXb0ozJfurQ59G2XcgAn3vROlSSnpbIvuhKrzL5jwWDTaYa5tVF1Zjwia/5HUhKBkcPuWGXg05nMjWhZfCuEetzMLoGcHmtvabugFrqsAg=" + def pub = fact.generatePublic(new X509EncodedKeySpec(TextCodec.BASE64.decode(publicKey))) + def v = new EllipticCurveSignatureValidator(SignatureAlgorithm.ES512, pub) + def verifier = { token -> + def signatureStart = token.lastIndexOf('.') + def withoutSignature = token.substring(0, signatureStart) + def signature = token.substring(signatureStart + 1) + assert v.isValid(withoutSignature.getBytes("US-ASCII"), TextCodec.BASE64URL.decode(signature)), "Signature do not match that of other implementations" + } + //Test verification for token created using https://github.com/auth0/node-jsonwebtoken/tree/v7.0.1 + verifier("eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30.Aab4x7HNRzetjgZ88AMGdYV2Ml7kzFbl8Ql2zXvBores7iRqm2nK6810ANpVo5okhHa82MQf2Q_Zn4tFyLDR9z4GAcKFdcAtopxq1h8X58qBWgNOc0Bn40SsgUc8wOX4rFohUCzEtnUREePsvc9EfXjjAH78WD2nq4tn-N94vf14SncQ") + //Test verification for token created using https://github.com/jwt/ruby-jwt/tree/v1.5.4 + verifier("eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzUxMiJ9.eyJ0ZXN0IjoidGVzdCJ9.AV26tERbSEwcoDGshneZmhokg-tAKUk0uQBoHBohveEd51D5f6EIs6cskkgwtfzs4qAGfx2rYxqQXr7LTXCNquKiAJNkTIKVddbPfped3_TQtmHZTmMNiqmWjiFj7Y9eTPMMRRu26w4gD1a8EQcBF-7UGgeH4L_1CwHJWAXGbtu7uMUn") + } + + @Test + void legacySignatureCompatTest() { + def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" + def keypair = EllipticCurveProvider.generateKeyPair() + def signature = Signature.getInstance(SignatureAlgorithm.ES512.jcaName, "BC") + def data = withoutSignature.getBytes("US-ASCII") + signature.initSign(keypair.private) + signature.update(data) + def signed = signature.sign() + assert new EllipticCurveSignatureValidator(SignatureAlgorithm.ES512, keypair.public).isValid(data, signed) + } + + @Test + void invalidAlgorithmTest() { + def invalidAlgorithm = SignatureAlgorithm.HS256 + try { + EllipticCurveProvider.getSignatureByteArrayLength(invalidAlgorithm) + fail() + } catch (JwtException e) { + assertEquals e.message, 'Unsupported Algorithm: ' + invalidAlgorithm.name() + } + } + + @Test + void invalidECDSASignatureFormatTest() { + try { + def signature = new byte[257] + SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(signature) + EllipticCurveProvider.transcodeSignatureToDER(signature) + fail() + } catch (JwtException e) { + assertEquals e.message, 'Invalid ECDSA signature format' + } + } + + @Test + void invalidDERSignatureToJoseFormatTest() { + def verify = { signature -> + try { + EllipticCurveProvider.transcodeSignatureToConcat(signature, 132) + fail() + } catch (JwtException e) { + assertEquals e.message, 'Invalid ECDSA signature format' + } + } + def signature = new byte[257] + SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(signature) + //invalid type + signature[0] = 34 + verify(signature) + def shortSignature = new byte[7] + SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(shortSignature) + verify(shortSignature) + signature[0] = 48 +// signature[1] = 0x81 + signature[1] = -10 + verify(signature) + } + + @Test + void edgeCaseSignatureLengthTest() { + def signature = new byte[1] + EllipticCurveProvider.transcodeSignatureToDER(signature) + } + + @Test + void edgeCaseSignatureToConcatLengthTest() { + try { + def signature = TextCodec.BASE64.decode("MIEAAGg3OVb/ZeX12cYrhK3c07TsMKo7Kc6SiqW++4CAZWCX72DkZPGTdCv2duqlupsnZL53hiG3rfdOLj8drndCU+KHGrn5EotCATdMSLCXJSMMJoHMM/ZPG+QOHHPlOWnAvpC1v4lJb32WxMFNz1VAIWrl9Aa6RPG1GcjCTScKjvEE") + EllipticCurveProvider.transcodeSignatureToConcat(signature, 132) + fail() + } catch (JwtException e) { + + } + } + + @Test + void edgeCaseSignatureToConcatInvalidSignatureTest() { + try { + def signature = TextCodec.BASE64.decode("MIGBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") + EllipticCurveProvider.transcodeSignatureToConcat(signature, 132) + fail() + } catch (JwtException e) { + assertEquals e.message, 'Invalid ECDSA signature format' + } + } + + @Test + void edgeCaseSignatureToConcatInvalidSignatureBranchTest() { + try { + def signature = TextCodec.BASE64.decode("MIGBAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") + EllipticCurveProvider.transcodeSignatureToConcat(signature, 132) + fail() + } catch (JwtException e) { + assertEquals e.message, 'Invalid ECDSA signature format' + } + } + + @Test + void edgeCaseSignatureToConcatInvalidSignatureBranch2Test() { + try { + def signature = TextCodec.BASE64.decode("MIGBAj4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") + EllipticCurveProvider.transcodeSignatureToConcat(signature, 132) + fail() + } catch (JwtException e) { + assertEquals e.message, 'Invalid ECDSA signature format' + } + } + + @Test + void verifySwarmTest() { + for (SignatureAlgorithm algorithm : [SignatureAlgorithm.ES256, SignatureAlgorithm.ES384, SignatureAlgorithm.ES512]) { + int i = 0 + while(i < 10) { + i++ + def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" + def keypair = EllipticCurveProvider.generateKeyPair() + def data = withoutSignature.getBytes("US-ASCII") + def signature = new EllipticCurveSigner(algorithm, keypair.private).sign(data) + assert new EllipticCurveSignatureValidator(algorithm, keypair.public).isValid(data, signature) + } + } + } } diff --git a/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy index 4587f1f6..02067309 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy @@ -15,15 +15,16 @@ */ package io.jsonwebtoken.impl.crypto +import io.jsonwebtoken.JwtException import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.SignatureException +import org.junit.Test import java.security.InvalidKeyException import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey -import org.junit.Test import static org.junit.Assert.* class EllipticCurveSignerTest { @@ -79,6 +80,35 @@ class EllipticCurveSignerTest { } } + @Test + void testDoSignWithJoseSignatureFormatException() { + + KeyPair kp = EllipticCurveProvider.generateKeyPair() + PublicKey publicKey = kp.getPublic(); + PrivateKey privateKey = kp.getPrivate(); + + String msg = 'foo' + final JwtException ex = new JwtException(msg) + + def signer = new EllipticCurveSigner(SignatureAlgorithm.ES256, privateKey) { + @Override + protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException, JwtException { + throw ex + } + } + + byte[] bytes = new byte[16] + SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(bytes) + + try { + signer.sign(bytes) + fail(); + } catch (SignatureException se) { + assertEquals se.message, 'Unable to convert signature to JOSE format. ' + msg + assertSame se.cause, ex + } + } + @Test void testDoSignWithJdkSignatureException() { diff --git a/src/test/groovy/io/jsonwebtoken/lang/StringsTest.groovy b/src/test/groovy/io/jsonwebtoken/lang/StringsTest.groovy index a06f5d57..def31169 100644 --- a/src/test/groovy/io/jsonwebtoken/lang/StringsTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/lang/StringsTest.groovy @@ -14,4 +14,46 @@ class StringsTest { assertTrue Strings.hasText(" foo "); assertTrue Strings.hasText("foo") } + + @Test + void testClean() { + assertEquals "this is a test", Strings.clean("this is a test") + assertEquals "this is a test", Strings.clean(" this is a test") + assertEquals "this is a test", Strings.clean(" this is a test ") + assertEquals "this is a test", Strings.clean("\nthis is a test \t ") + assertNull Strings.clean(null) + assertNull Strings.clean("") + assertNull Strings.clean("\t") + assertNull Strings.clean(" ") + } + + @Test + void testCleanCharSequence() { + def result = Strings.clean(new StringBuilder("this is a test")) + assertNotNull result + assertEquals "this is a test", result.toString() + + result = Strings.clean(new StringBuilder(" this is a test")) + assertNotNull result + assertEquals "this is a test", result.toString() + + result = Strings.clean(new StringBuilder(" this is a test ")) + assertNotNull result + assertEquals "this is a test", result.toString() + + result = Strings.clean(new StringBuilder("\nthis is a test \t ")) + assertNotNull result + assertEquals "this is a test", result.toString() + + assertNull Strings.clean((StringBuilder) null) + assertNull Strings.clean(new StringBuilder("")) + assertNull Strings.clean(new StringBuilder("\t")) + assertNull Strings.clean(new StringBuilder(" ")) + } + + + @Test + void testTrimWhitespace() { + assertEquals "", Strings.trimWhitespace(" ") + } }