Merge branch 'master' into jwe

# Conflicts:
#	pom.xml
This commit is contained in:
Les Hazlewood 2016-10-07 13:48:46 -07:00
commit 89613e6520
21 changed files with 1180 additions and 397 deletions

3
.gitignore vendored
View File

@ -17,3 +17,6 @@ target/
*.iml
*.iws
.classpath
.project
.settings

View File

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

302
CHANGELOG.md Normal file
View File

@ -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<Claims> 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<Claims> 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<T>() {
@Override
public T onPlaintextJwt(Jwt<Header, String> 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<Header, Claims> 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<String> 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<Claims> 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<Jwt<Header, T>>() {
@Override
public T onPlaintextJws(Jws<String> 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<Claims> 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<Header,String> jwt = Jwts.parser().parsePlaintextJwt(compact);
//for a known Claims JWT string:
Jwt<Header,Claims> jwt = Jwts.parser().parseClaimsJwt(compact);
//for a known signed plaintext JWT (aka a plaintext JWS):
Jws<String> jws = Jwts.parser().setSigningKey(key).parsePlaintextJws(compact);
//for a known signed Claims JWT (aka a Claims JWS):
Jws<Claims> jws = Jwts.parser().setSigningKey(key).parseClaimsJws(compact);
```

385
README.md
View File

@ -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:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.6.0</version>
<version>0.7.0</version>
</dependency>
```
@ -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> 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<Claims> 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<Claims> 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<T>() {
@Override
public T onPlaintextJwt(Jwt<Header, String> 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<Header, Claims> 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<String> 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<Claims> 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<Jwt<Header, T>>() {
@Override
public T onPlaintextJws(Jws<String> 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<Claims> 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<Header,String> jwt = Jwts.parser().parsePlaintextJwt(compact);
//for a known Claims JWT string:
Jwt<Header,Claims> jwt = Jwts.parser().parseClaimsJwt(compact);
//for a known signed plaintext JWT (aka a plaintext JWS):
Jws<String> jws = Jwts.parser().setSigningKey(key).parsePlaintextJws(compact);
//for a known signed Claims JWT (aka a Claims JWS):
Jws<Claims> jws = Jwts.parser().setSigningKey(key).parseClaimsJws(compact);
```
- [JJWT Changelog](CHANGELOG.md)
<a name="olderJackson"></a>
#### 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
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.4.2</version>
<version>2.8.2</version>
</dependency>
```
@ -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).
This project is open-source via the [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0).

98
pom.xml
View File

@ -25,7 +25,7 @@
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0-SNAPSHOT</version>
<version>0.8.0-SNAPSHOT</version>
<name>JSON Web Token support for the JVM</name>
<packaging>jar</packaging>
@ -54,25 +54,25 @@
<properties>
<maven.jar.version>2.4</maven.jar.version>
<maven.compiler.version>3.1</maven.compiler.version>
<maven.jar.version>3.0.2</maven.jar.version>
<maven.compiler.version>3.5.1</maven.compiler.version>
<jdk.version>1.6</jdk.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<buildNumber>${user.name}-${maven.build.timestamp}</buildNumber>
<jackson.version>2.7.0</jackson.version>
<jackson.version>2.8.2</jackson.version>
<!-- Optional Runtime Dependencies: -->
<bouncycastle.version>1.51</bouncycastle.version>
<bouncycastle.version>1.55</bouncycastle.version>
<!-- Test Dependencies: Only required for testing when building. Not required by users at runtime: -->
<groovy.version>2.3.0-beta-2</groovy.version>
<logback.version>1.0.7</logback.version>
<easymock.version>3.3.1</easymock.version>
<groovy.version>2.4.7</groovy.version>
<logback.version>1.1.7</logback.version>
<easymock.version>3.4</easymock.version>
<junit.version>4.12</junit.version>
<powermock.version>1.6.2</powermock.version>
<failsafe.plugin.version>2.12.4</failsafe.plugin.version>
<powermock.version>1.6.5</powermock.version>
<failsafe.plugin.version>2.19.1</failsafe.plugin.version>
</properties>
@ -270,79 +270,19 @@
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>cobertura-maven-plugin</artifactId>
<version>2.7</version>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.7.6.201602180812</version>
<configuration>
<maxmem>256m</maxmem>
<aggregate>true</aggregate>
<instrumentation>
<excludes>
<exclude>io.jsonwebtoken.lang.*</exclude>
</excludes>
</instrumentation>
<check>
<lineRate>100</lineRate>
<branchRate>100</branchRate>
<!-- <packageLineRate>100</packageLineRate>
<packageBranchRate>100</packageBranchRate> -->
<haltOnFailure>true</haltOnFailure>
<regexes>
<regex>
<!-- Work in progress: -->
<pattern>io.jsonwebtoken.impl.serialization.*</pattern>
<branchRate>0</branchRate>
<lineRate>0</lineRate>
</regex>
<regex>
<!-- Work in progress: -->
<pattern>io.jsonwebtoken.impl.DefaultJweFactory</pattern>
<branchRate>0</branchRate>
<lineRate>0</lineRate>
</regex>
<regex>
<!-- Work in progress: -->
<pattern>io.jsonwebtoken.impl.DefaultJweHeader</pattern>
<branchRate>0</branchRate>
<lineRate>0</lineRate>
</regex>
<regex>
<!-- Work in progress: -->
<pattern>io.jsonwebtoken.impl.DispatchingParser</pattern>
<branchRate>0</branchRate>
<lineRate>0</lineRate>
</regex>
<regex>
<!-- This was pulled in from another project (without pulling in the tests) -
we don't care about coverage here really.: -->
<pattern>io.jsonwebtoken.lang.*</pattern>
<branchRate>0</branchRate>
<lineRate>0</lineRate>
</regex>
<regex>
<!-- Cannot get to 100% on DefaultClaims because of Cobertura bug w/ generics:
https://github.com/cobertura/cobertura/issues/207 -->
<pattern>io.jsonwebtoken.impl.DefaultClaims</pattern>
<lineRate>97</lineRate>
<branchRate>100</branchRate>
</regex>
<regex>
<pattern>io.jsonwebtoken.impl.DefaultJwtParser</pattern>
<lineRate>100</lineRate>
<branchRate>90</branchRate>
</regex>
</regexes>
</check>
<formats>
<format>xml</format>
<format>html</format>
</formats>
<excludes>
<exclude>**/io/jsonwebtoken/lang/*</exclude>
</excludes>
</configuration>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>clean</goal>
<goal>check</goal>
<goal>prepare-agent</goal>
</goals>
</execution>
</executions>
@ -398,7 +338,6 @@
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>jdk8</id>
@ -465,5 +404,4 @@
</build>
</profile>
</profiles>
</project>

View File

@ -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.

View File

@ -120,10 +120,25 @@ public class DefaultClaims extends JwtMap implements Claims {
value = getDate(claimName);
}
return castClaimValue(value, requiredType);
}
private <T> T castClaimValue(Object value, Class<T> requiredType) {
if (requiredType == Date.class && value instanceof Long) {
value = new Date((Long)value);
}
if (value instanceof Integer) {
int intValue = (Integer) value;
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());
}

View File

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

View File

@ -47,10 +47,16 @@ public class JwtMap implements Map<String,Object> {
} 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<String,Object> {
public String toString() {
return map.toString();
}
@Override
public int hashCode() {
return map.hashCode();
}
@Override
public boolean equals(Object obj) {
return map.equals(obj);
}
}

View File

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

View File

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

View File

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

View File

@ -69,7 +69,7 @@ public abstract class MacProvider extends SignatureProvider {
*
* <table> <thead> <tr> <th>Signature Algorithm</th> <th>Generated Key Size</th> </tr> </thead> <tbody> <tr>
* <td>HS256</td> <td>256 bits (32 bytes)</td> </tr> <tr> <td>HS384</td> <td>384 bits (48 bytes)</td> </tr> <tr>
* <td>HS512</td> <td>256 bits (64 bytes)</td> </tr> </tbody> </table>
* <td>HS512</td> <td>512 bits (64 bytes)</td> </tr> </tbody> </table>
*
* @param alg the signature algorithm that will be used with the generated key
* @param random the secure random number generator used during key generation

View File

@ -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 {
/**

View File

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

View File

@ -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<Header,Claims> 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<Header,Claims> 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<Header,Claims> 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

View File

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

View File

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

View File

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

View File

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

View File

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