Merge pull request #1 from jwtk/master

Merge Updates from Upstream Master
This commit is contained in:
brentstormpath 2016-07-12 17:19:12 -07:00 committed by GitHub
commit 007b82c6ad
34 changed files with 1113 additions and 374 deletions

View File

@ -2,13 +2,12 @@
language: java
jdk:
- openjdk7
- oraclejdk7
- oraclejdk8
before_install:
- export BUILD_COVERAGE="$([ $TRAVIS_JDK_VERSION == 'openjdk7' ] && echo 'true')"
- export BUILD_COVERAGE="$([ $TRAVIS_JDK_VERSION == 'oraclejdk8' ] && echo 'true')"
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

244
CHANGELOG.md Normal file
View File

@ -0,0 +1,244 @@
## 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);
```

373
README.md
View File

@ -1,11 +1,60 @@
[![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.
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
@ -31,7 +80,7 @@ dependencies {
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 +94,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 +137,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:
@ -91,12 +155,55 @@ try {
* ES384: ECDSA using P-384 and SHA-384
* ES512: ECDSA using P-512 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 calims *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,251 +212,7 @@ 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?
@ -370,4 +233,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).

80
pom.xml
View File

@ -61,7 +61,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<buildNumber>${user.name}-${maven.build.timestamp}</buildNumber>
<jackson.version>2.4.2</jackson.version>
<jackson.version>2.7.0</jackson.version>
<!-- Optional Runtime Dependencies: -->
<bouncycastle.version>1.51</bouncycastle.version>
@ -270,66 +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/*.class</exclude>
</excludes>
<ignores>
<ignore>io.jsonwebtoken.lang.*</ignore>
</ignores>
</instrumentation>
<check>
<lineRate>100</lineRate>
<branchRate>100</branchRate>
<packageLineRate>100</packageLineRate>
<packageBranchRate>100</packageBranchRate>
<haltOnFailure>true</haltOnFailure>
<regexes>
<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>96</lineRate>
<branchRate>100</branchRate>
</regex>
<regex>
<pattern>io.jsonwebtoken.impl.DefaultJwtBuilder</pattern>
<lineRate>91</lineRate>
<branchRate>85</branchRate>
</regex>
<regex>
<pattern>io.jsonwebtoken.impl.DefaultJwtParser</pattern>
<lineRate>97</lineRate>
<branchRate>88</branchRate>
</regex>
<regex>
<pattern>io.jsonwebtoken.impl.crypto.EllipticCurveSignatureValidator</pattern>
<lineRate>95</lineRate>
<branchRate>100</branchRate>
</regex>
<regex>
<pattern>io.jsonwebtoken.impl.crypto.RsaSignatureValidator</pattern>
<lineRate>93</lineRate>
<branchRate>100</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>
@ -355,7 +308,7 @@
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<version>2.3.5</version>
<version>3.0.1</version>
<extensions>true</extensions>
<executions>
<execution>
@ -366,6 +319,17 @@
</goals>
</execution>
</executions>
<configuration>
<instructions>
<Import-Package><![CDATA[
android.util;resolution:=optional,
org.bouncycastle.jce;resolution:=optional,
org.bouncycastle.jce.spec;resolution:=optional,
*
]]>
</Import-Package>
</instructions>
</configuration>
</plugin>
<plugin>
<groupId>org.eluder.coveralls</groupId>
@ -374,7 +338,6 @@
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>jdk8</id>
@ -441,5 +404,4 @@
</build>
</profile>
</profiles>
</project>

View File

@ -0,0 +1,18 @@
package io.jsonwebtoken;
import java.util.Date;
/**
* A clock represents a time source that can be used when creating and verifying JWTs.
*
* @since 0.7.0
*/
public interface Clock {
/**
* Returns the clock's current timestamp at the instant the method is invoked.
*
* @return the clock's current timestamp at the instant the method is invoked.
*/
Date now();
}

View File

@ -0,0 +1,34 @@
package io.jsonwebtoken;
import io.jsonwebtoken.impl.compression.DeflateCompressionCodec;
import io.jsonwebtoken.impl.compression.GzipCompressionCodec;
/**
* Provides default implementations of the {@link CompressionCodec} interface.
*
* @see #DEFLATE
* @see #GZIP
* @since 0.7.0
*/
public final class CompressionCodecs {
private static final CompressionCodecs INSTANCE = new CompressionCodecs();
private CompressionCodecs() {} //prevent external instantiation
/**
* Codec implementing the <a href="https://tools.ietf.org/html/rfc7518">JWA</a> standard
* <a href="https://en.wikipedia.org/wiki/DEFLATE">deflate</a> compression algorithm
*/
public static final CompressionCodec DEFLATE = new DeflateCompressionCodec();
/**
* Codec implementing the <a href="https://en.wikipedia.org/wiki/Gzip">gzip</a> compression algorithm.
* <h5>Compatibility Warning</h5>
* <p><b>This is not a standard JWA compression algorithm</b>. Be sure to use this only when you are confident
* that all parties accessing the token support the gzip algorithm.</p>
* <p>If you're concerned about compatibility, the {@link #DEFLATE DEFLATE} code is JWA standards-compliant.</p>
*/
public static final CompressionCodec GZIP = new GzipCompressionCodec();
}

View File

@ -48,8 +48,13 @@ public interface Header<T extends Header<T>> extends Map<String,Object> {
/** JWT {@code Content Type} header parameter name: <code>"cty"</code> */
public static final String CONTENT_TYPE = "cty";
/** JWT {@code Compression Algorithm} header parameter name: <code>"calg"</code> */
public static final String COMPRESSION_ALGORITHM = "calg";
/** JWT {@code Compression Algorithm} header parameter name: <code>"zip"</code> */
public static final String COMPRESSION_ALGORITHM = "zip";
/** JJWT legacy/deprecated compression algorithm header parameter name: <code>"calg"</code>
* @deprecated use {@link #COMPRESSION_ALGORITHM} instead. */
@Deprecated
public static final String DEPRECATED_COMPRESSION_ALGORITHM = "calg";
/**
* Returns the <a href="https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25#section-5.1">

View File

@ -356,12 +356,18 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
* can be useful. For example, when embedding JWTs in URLs, some browsers may not support URLs longer than a
* certain length. Using compression can help ensure the compact JWT fits within that length. However, NOTE:</p>
*
* <p><b>WARNING:</b> Compression is not defined by the JWT Specification, and it is not expected that other libraries
* (including JJWT versions < 0.6.0) are able to consume a compressed JWT body correctly. Only use this method
* if you are sure that you will consume the JWT with JJWT >= 0.6.0 or another library that you know implements
* the same behavior.</p>
* <h5>Compatibility Warning</h5>
*
* @see io.jsonwebtoken.impl.compression.CompressionCodecs
* <p>The JWT family of specifications defines compression only for JWE (Json Web Encryption)
* tokens. Even so, JJWT will also support compression for JWS tokens as well if you choose to use it.
* However, be aware that <b>if you use compression when creating a JWS token, other libraries may not be able to
* parse that JWS token</b>. When using compression for JWS tokens, be sure that that all parties accessing the
* JWS token support compression for JWS.</p>
*
* <p>Compression when creating JWE tokens however should be universally accepted for any
* library that supports JWE.</p>
*
* @see io.jsonwebtoken.CompressionCodecs
*
* @param codec implementation of the {@link CompressionCodec} to be used.
* @return the builder for method chaining.

View File

@ -15,6 +15,8 @@
*/
package io.jsonwebtoken;
import io.jsonwebtoken.impl.DefaultClock;
import java.security.Key;
import java.util.Date;
@ -124,6 +126,16 @@ public interface JwtParser {
*/
JwtParser require(String claimName, Object value);
/**
* Sets the {@link Clock} that determines the timestamp to use when validating the parsed JWT.
* 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.
* @since 0.7.0
*/
JwtParser setClock(Clock clock);
/**
* 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

@ -0,0 +1,28 @@
package io.jsonwebtoken.impl;
import io.jsonwebtoken.Clock;
import java.util.Date;
/**
* Default {@link Clock} implementation.
*
* @since 0.7.0
*/
public class DefaultClock implements Clock {
/**
* Default static instance that may be shared. It is thread-safe.
*/
public static final Clock INSTANCE = new DefaultClock();
/**
* Simply returns <code>new {@link Date}()</code>.
*
* @return a new {@link Date} instance.
*/
@Override
public Date now() {
return new Date();
}
}

View File

@ -16,6 +16,7 @@
package io.jsonwebtoken.impl;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.lang.Strings;
import java.util.Map;
@ -52,9 +53,14 @@ public class DefaultHeader<T extends Header<T>> extends JwtMap implements Header
return (T)this;
}
@SuppressWarnings("deprecation")
@Override
public String getCompressionAlgorithm() {
return getString(COMPRESSION_ALGORITHM);
String alg = getString(COMPRESSION_ALGORITHM);
if (!Strings.hasText(alg)) {
alg = getString(DEPRECATED_COMPRESSION_ALGORITHM);
}
return alg;
}
@Override

View File

@ -18,6 +18,7 @@ package io.jsonwebtoken.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.ClaimJwtException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Clock;
import io.jsonwebtoken.CompressionCodec;
import io.jsonwebtoken.CompressionCodecResolver;
import io.jsonwebtoken.ExpiredJwtException;
@ -69,6 +70,8 @@ public class DefaultJwtParser implements JwtParser {
Claims expectedClaims = new DefaultClaims();
private Clock clock = DefaultClock.INSTANCE;
@Override
public JwtParser requireIssuedAt(Date issuedAt) {
expectedClaims.setIssuedAt(issuedAt);
@ -127,6 +130,13 @@ public class DefaultJwtParser implements JwtParser {
return this;
}
@Override
public JwtParser setClock(Clock clock) {
Assert.notNull(clock, "Clock instance cannot be null.");
this.clock = clock;
return this;
}
@Override
public JwtParser setSigningKey(byte[] key) {
Assert.notEmpty(key, "signing key cannot be null or empty.");
@ -309,8 +319,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());
}
@ -346,16 +356,15 @@ public class DefaultJwtParser implements JwtParser {
//since 0.3:
if (claims != null) {
Date now = null;
SimpleDateFormat sdf;
final Date now = this.clock.now();
//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) {
now = new Date();
if (now.equals(exp) || now.after(exp)) {
sdf = new SimpleDateFormat(ISO_8601_FORMAT);
String expVal = sdf.format(exp);
@ -371,10 +380,6 @@ public class DefaultJwtParser implements JwtParser {
Date nbf = claims.getNotBefore();
if (nbf != null) {
if (now == null) {
now = new Date();
}
if (now.before(nbf)) {
sdf = new SimpleDateFormat(ISO_8601_FORMAT);
String nbfVal = sdf.format(nbf);

View File

@ -0,0 +1,39 @@
package io.jsonwebtoken.impl;
import io.jsonwebtoken.Clock;
import java.util.Date;
/**
* A {@code Clock} implementation that is constructed with a seed timestamp and always reports that same
* timestamp.
*
* @since 0.7.0
*/
public class FixedClock implements Clock {
private final Date now;
/**
* Creates a new fixed clock using <code>new {@link Date Date}()</code> as the seed timestamp. All calls to
* {@link #now now()} will always return this seed Date.
*/
public FixedClock() {
this(new Date());
}
/**
* Creates a new fixed clock using the specified seed timestamp. All calls to
* {@link #now now()} will always return this seed Date.
*
* @param now the specified Date to always return from all calls to {@link #now now()}.
*/
public FixedClock(Date now) {
this.now = now;
}
@Override
public Date now() {
return this.now;
}
}

View File

@ -24,7 +24,9 @@ import io.jsonwebtoken.CompressionCodec;
* @see #GZIP
*
* @since 0.6.0
* @deprecated use {@link io.jsonwebtoken.CompressionCodecs} instead.
*/
@Deprecated
public final class CompressionCodecs {
private static final CompressionCodecs I = new CompressionCodecs();
@ -33,12 +35,16 @@ public final class CompressionCodecs {
/**
* Codec implementing the <a href="https://en.wikipedia.org/wiki/DEFLATE">deflate</a> compression algorithm
* @deprecated use {@link io.jsonwebtoken.CompressionCodecs#DEFLATE} instead.
*/
public static final CompressionCodec DEFLATE = new DeflateCompressionCodec();
@Deprecated
public static final CompressionCodec DEFLATE = io.jsonwebtoken.CompressionCodecs.DEFLATE;
/**
* Codec implementing the <a href="https://en.wikipedia.org/wiki/Gzip">gzip</a> compression algorithm
* @deprecated use {@link io.jsonwebtoken.CompressionCodecs#GZIP} instead.
*/
public static final CompressionCodec GZIP = new GzipCompressionCodec();
@Deprecated
public static final CompressionCodec GZIP = io.jsonwebtoken.CompressionCodecs.GZIP;
}

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

@ -0,0 +1,20 @@
package io.jsonwebtoken.lang;
/**
* @since 0.6
*/
public final class Arrays {
//for code coverage
private static final Arrays INSTANCE = new Arrays();
private Arrays(){}
public static int length(byte[] bytes) {
return bytes != null ? bytes.length : 0;
}
public static byte[] clean(byte[] bytes) {
return length(bytes) > 0 ? bytes : null;
}
}

View File

@ -18,7 +18,12 @@ package io.jsonwebtoken.lang;
import java.util.Collection;
import java.util.Map;
public abstract class Assert {
public final class Assert {
//for code coverage
private static final Assert INSTANCE = new Assert();
private Assert(){}
/**
* Assert a boolean expression, throwing <code>IllegalArgumentException</code>

View File

@ -21,7 +21,11 @@ import java.lang.reflect.Constructor;
/**
* @since 0.1
*/
public class Classes {
public final class Classes {
private static final Classes INSTANCE = new Classes();
private Classes() {}
/**
* @since 0.1
@ -79,7 +83,7 @@ public class Classes {
if (clazz == null) {
String msg = "Unable to load class named [" + fqcn + "] from the thread context, current, or " +
"system/application ClassLoaders. All heuristics have been exhausted. Class could not be found.";
"system/application ClassLoaders. All heuristics have been exhausted. Class could not be found.";
if (fqcn != null && fqcn.startsWith("com.stormpath.sdk.impl")) {
msg += " Have you remembered to include the stormpath-sdk-impl .jar in your runtime classpath?";
@ -100,7 +104,7 @@ public class Classes {
*
* @param name the name of the resource to acquire from the classloader(s).
* @return the InputStream of the resource found, or <code>null</code> if the resource cannot be found from any
* of the three mentioned ClassLoaders.
* of the three mentioned ClassLoaders.
* @since 0.8
*/
public static InputStream getResourceAsStream(String name) {
@ -181,6 +185,7 @@ public class Classes {
*/
private static interface ClassLoaderAccessor {
Class loadClass(String fqcn);
InputStream getResourceStream(String name);
}

View File

@ -24,7 +24,12 @@ import java.util.List;
import java.util.Map;
import java.util.Properties;
public abstract class Collections {
public final class Collections {
//for code coverage
private static final Collections INSTANCE = new Collections();
private Collections(){}
/**
* Return <code>true</code> if the supplied Collection is <code>null</code>

View File

@ -20,7 +20,12 @@ import java.io.IOException;
import java.lang.reflect.Array;
import java.util.Arrays;
public abstract class Objects {
public final class Objects {
//for code coverage
private static final Objects INSTANCE = new Objects();
private Objects(){}
private static final int INITIAL_HASH = 7;
private static final int MULTIPLIER = 31;

View File

@ -19,7 +19,11 @@ import java.security.Provider;
import java.security.Security;
import java.util.concurrent.atomic.AtomicBoolean;
public class RuntimeEnvironment {
public final class RuntimeEnvironment {
private static final RuntimeEnvironment INSTANCE = new RuntimeEnvironment();
private RuntimeEnvironment(){}
private static final String BC_PROVIDER_CLASS_NAME = "org.bouncycastle.jce.provider.BouncyCastleProvider";

View File

@ -29,7 +29,9 @@ import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeSet;
public abstract class Strings {
public final class Strings {
private static final Strings INSTANCE = new Strings(); //for code coverage
private static final String FOLDER_SEPARATOR = "/";
@ -43,6 +45,8 @@ public abstract class Strings {
public static final Charset UTF_8 = Charset.forName("UTF-8");
private Strings(){}
//---------------------------------------------------------------------
// General convenience methods for working with Strings
//---------------------------------------------------------------------

View File

@ -23,12 +23,13 @@ package io.jsonwebtoken.lang;
*/
public class UnknownClassException extends RuntimeException {
/*
/**
* Creates a new UnknownClassException.
*/
*
public UnknownClassException() {
super();
}
}*/
/**
* Constructs a new UnknownClassException.
@ -39,11 +40,11 @@ public class UnknownClassException extends RuntimeException {
super(message);
}
/**
/*
* Constructs a new UnknownClassException.
*
* @param cause the underlying Throwable that caused this exception to be thrown.
*/
*
public UnknownClassException(Throwable cause) {
super(cause);
}
@ -53,9 +54,10 @@ public class UnknownClassException extends RuntimeException {
*
* @param message the reason for the exception
* @param cause the underlying Throwable that caused this exception to be thrown.
*/
*
public UnknownClassException(String message, Throwable cause) {
super(message, cause);
}
*/
}

View File

@ -15,6 +15,8 @@
*/
package io.jsonwebtoken
import io.jsonwebtoken.impl.DefaultClock
import io.jsonwebtoken.impl.FixedClock
import io.jsonwebtoken.impl.TextCodec
import org.junit.Test
@ -1392,4 +1394,39 @@ class JwtParserTest {
)
}
}
@Test
void testParseClockManipulationWithFixedClock() {
def then = System.currentTimeMillis() - 1000
Date expiry = new Date(then)
Date beforeExpiry = new Date(then - 1000)
String compact = Jwts.builder().setSubject('Joe').setExpiration(expiry).compact()
Jwts.parser().setClock(new FixedClock(beforeExpiry)).parse(compact)
}
@Test
void testParseClockManipulationWithNullClock() {
JwtParser parser = Jwts.parser();
try {
parser.setClock(null)
fail()
} catch (IllegalArgumentException expected) {
}
}
@Test
void testParseClockManipulationWithDefaultClock() {
Date expiry = new Date(System.currentTimeMillis() - 1000)
String compact = Jwts.builder().setSubject('Joe').setExpiration(expiry).compact()
try {
Jwts.parser().setClock(new DefaultClock()).parse(compact)
fail()
} catch (ExpiredJwtException e) {
assertTrue e.getMessage().startsWith('JWT expired at ')
}
}
}

View File

@ -15,6 +15,7 @@
*/
package io.jsonwebtoken.impl
import io.jsonwebtoken.Header
import org.junit.Test
import static org.junit.Assert.*
@ -37,4 +38,18 @@ class DefaultHeaderTest {
h.setContentType('bar')
assertEquals h.getContentType(), 'bar'
}
@Test
void testSetCompressionAlgorithm() {
def h = new DefaultHeader()
h.setCompressionAlgorithm("DEF")
assertEquals "DEF", h.getCompressionAlgorithm()
}
@Test
void testBackwardsCompatibleCompressionHeader() {
def h = new DefaultHeader()
h.put(Header.DEPRECATED_COMPRESSION_ALGORITHM, "DEF")
assertEquals "DEF", h.getCompressionAlgorithm()
}
}

View File

@ -279,4 +279,25 @@ class DefaultJwtBuilderTest {
assertNull b.claims
}
@Test
void testSetNullExpirationWithNullClaims() {
def b = new DefaultJwtBuilder()
b.setExpiration(null)
assertNull b.claims
}
@Test
void testSetNullNotBeforeWithNullClaims() {
def b = new DefaultJwtBuilder()
b.setNotBefore(null)
assertNull b.claims
}
@Test
void testSetNullIssuedAtWithNullClaims() {
def b = new DefaultJwtBuilder()
b.setIssuedAt(null)
assertNull b.claims
}
}

View File

@ -0,0 +1,19 @@
package io.jsonwebtoken.impl
import org.junit.Test
import static org.junit.Assert.*
class FixedClockTest {
@Test
void testFixedClockDefaultConstructor() {
def clock = new FixedClock()
def date1 = clock.now()
Thread.sleep(100)
def date2 = clock.now()
assertSame date1, date2
}
}

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

@ -0,0 +1,17 @@
package io.jsonwebtoken.lang
import org.junit.Test
import static org.junit.Assert.*
class StringsTest {
@Test
void testHasText() {
assertFalse Strings.hasText(null)
assertFalse Strings.hasText("")
assertFalse Strings.hasText(" ")
assertTrue Strings.hasText(" foo ");
assertTrue Strings.hasText("foo")
}
}