Skip to content

Commit

Permalink
RFC7797 implementation (jwtk#832)
Browse files Browse the repository at this point in the history
Closes jwtk#515

[RFC 7797)(https://www.rfc-editor.org/rfc/rfc7797.html) support.

JwsHeader:
- Added new JwtHeader#isPayloadEncoded() to check for `b64` in crit values and as parameter value.

JwtBuilder:
- Added new #content(String) method for non-detached unencoded payloads
- Added new #encodePayload(boolean) method to disable payload Base64URL-encoding

JwtParserBuilder:
- Added new #critical(String) convenience method to append to the crit set

JwtParser:
- Added new #parseContentJws(String, byte[]) method to allow supplying detached payloads for signature verification at parse time
- Added new #parseClaimsJws(String, byte[]) method to allow supplying detached serialized JSON claims for signature verification at parse time

- Added all backing implementations and test cases to 100% coverage
- Added RFC7797Test class for bulk of this RFC's tests so they don't get 'lost' in the already-too-large JwtsTest class
- README.md documentation added in JWS section
- CHANGELOG.md updated
  • Loading branch information
lhazlewood committed Sep 16, 2023
1 parent bf5d81c commit 34aa334
Show file tree
Hide file tree
Showing 18 changed files with 984 additions and 114 deletions.
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

### JJWT_RELEASE_VERSION

This is a big release! JJWT now fully supports Encrypted JSON Web Tokens (JWE) and JSON Web Keys (JWK)! See the
This is a big release! JJWT now fully supports Encrypted JSON Web Tokens (JWE), JSON Web Keys (JWK) and more! See the
sections below enumerating all new features as well as important notes on breaking changes or backwards-incompatible
changes made in preparation for the upcoming 1.0 release.

Expand Down Expand Up @@ -70,6 +70,17 @@ Many JJWT users won't need to use JWKs explicitly, but some JWA Key Management A
vectors) utilize JWKs when transmitting JWEs. As this was required by JWE, it is now implemented in full for
JWE use as well as general-purpose JWK support.

#### JWK Thumbprint and JWK Thumbprint URI support

The [JWK Thumbprint](https://www.rfc-editor.org/rfc/rfc7638.html) and
[JWK Thumbprint URI](https://www.rfc-editor.org/rfc/rfc9278.html) RFC specifications are now fully supported. Please
see the README.md file's corresponding named sections for both for full documentation and usage examples.

#### JWS Unencoded Payload Option (`b64`) support

The [JSON Web Signature (JWS) Unencoded Payload Option](https://www.rfc-editor.org/rfc/rfc7797.html) RFC specification
is now fully supported. Please see the README.md corresponding named section for documentation and usage examples.

#### Better PKCS11 and Hardware Security Module (HSM) support

Previous versions of JJWT enforced that Private Keys implemented the `RSAKey` and `ECKey` interfaces to enforce key
Expand Down
187 changes: 173 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
[![Build Status](https://github.com/jwtk/jjwt/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/jwtk/jjwt/actions/workflows/ci.yml?query=branch%3Amaster)
[![Coverage Status](https://coveralls.io/repos/github/jwtk/jjwt/badge.svg?branch=master)](https://coveralls.io/github/jwtk/jjwt?branch=master)
[![Gitter](https://badges.gitter.im/jwtk/jjwt.svg)](https://gitter.im/jwtk/jjwt?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)

# 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
and Android.
JJWT aims to be the easiest to use and understand library for creating and verifying JSON Web Tokens (JWTs) and
JSON Web Keys (JWKs) on the JVM and Android.

JJWT is a pure Java implementation based exclusively on the [JWT](https://tools.ietf.org/html/rfc7519),
[JWS](https://tools.ietf.org/html/rfc7515), [JWE](https://tools.ietf.org/html/rfc7516),
[JWA](https://tools.ietf.org/html/rfc7518), [JWK](https://tools.ietf.org/html/rfc7517),
[Octet JWK](https://www.rfc-editor.org/rfc/rfc8037),
[JWK Thumbprint](https://www.rfc-editor.org/rfc/rfc7638.html), and
[JWK Thumbprint URI](https://www.rfc-editor.org/rfc/rfc9278.html) RFC specifications and
open source under the terms of the [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0).
JJWT is a pure Java implementation based exclusively on the
[JOSE Working Group](https://datatracker.ietf.org/wg/jose/documents/) RFC specifications:

The library was created by [Les Hazlewood](https://github.com/lhazlewood)
* [RFC 7519: JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519)
* [RFC 7515: JSON Web Signature (JWS)](https://tools.ietf.org/html/rfc7515)
* [RFC 7516: JSON Web Encryption (JWE)](https://tools.ietf.org/html/rfc7516)
* [RFC 7517: JSON Web Key (JWK)](https://tools.ietf.org/html/rfc7517)
* [RFC 7518: JSON Web Algorithms (JWA)](https://tools.ietf.org/html/rfc7518)
* [RFC 7638: JSON Web Key Thumbprint](https://www.rfc-editor.org/rfc/rfc7638.html)
* [RFC 9278: JSON Web Key Thumbprint URI](https://www.rfc-editor.org/rfc/rfc9278.html)
* [RFC 7797: JWS Unencoded Payload Option](https://www.rfc-editor.org/rfc/rfc7797.html)
* [RFC 8037: Edwards Curve algorithms and JWKs](https://www.rfc-editor.org/rfc/rfc8037)

It was created by [Les Hazlewood](https://github.com/lhazlewood)
and is supported and 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 JWS compression and claim
enforcement.
JJWT is open source under the terms of the [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0).

## Table of Contents

Expand Down Expand Up @@ -157,7 +160,7 @@ enforcement.
* Convenient and readable [fluent](http://en.wikipedia.org/wiki/Fluent_interface) interfaces, great for IDE
auto-completion to write code quickly
* Fully RFC specification compliant on all implemented functionality, tested against RFC-specified test vectors
* Stable implementation with over 1,400+ tests and enforced 100% test code coverage. Every single method, statement
* Stable implementation with over 1,500+ tests and enforced 100% test code coverage. Every single method, statement
and conditional branch variant in the entire codebase is tested and required to pass on every build.
* Creating, parsing and verifying digitally signed compact JWTs (aka JWSs) with all standard JWS algorithms:

Expand Down Expand Up @@ -1817,6 +1820,162 @@ If you used JJWT to compress a JWS and you used a custom compression algorithm,

Please see the [Compression](#compression) section below to see how to decompress JWTs during parsing.

<a name="jws-unencoded"></a>
### Unencoded Payload Option

In some cases, especially if a JWS payload is large, it could be desirable to _not_ Base64URL-encode the JWS payload,
or even exclude the payload from the compact JWS string entirely. The JWT RFC specifications provide support
for these use cases via the
[JSON Web Signature (JWS) Unencoded Payload Option](https://www.rfc-editor.org/rfc/rfc7797.html) specification,
which JJWT supports.

This option comes with both benefits and disadvantages:

#### Benefits

A JWS producer can still create a JWS string to use for payload integrity verification without having to either:

1. Base64URL-encode the (potentially very large) payload, saving the time that could take.


2. Include the payload in the compact JWS string at all. Omitting the payload from the JWS compact string
entirely produces smaller JWSs that can be more efficient to transfer.

#### Disadvantages

1. Your application, and not JJWT, incurs the responsibility to ensure the payload is not modified during transmission
so the recipient can verify the JWS signature. For example, by using a sufficiently strong TLS (https) cipher
suite as well as any additional care before and after transmission, since
[TLS does not guarantee end-to-end security](https://tozny.com/blog/end-to-end-encryption-vs-https/).


2. If you choose to include the unencoded payload in the JWS compact string, your application
[MUST](https://www.rfc-editor.org/rfc/rfc7797.html#section-5.2) ensure that the payload does not contain a
period (`.`) character anywhere in the payload. The JWS recipient will experience parsing errors otherwise.


Before attempting to use this option, one should be aware of the RFC's
[security considerations](https://www.rfc-editor.org/rfc/rfc7797.html#section-8) first.
> **Note**
>
> **Protected JWS Only**
>
> The RFC specification defines the Unencoded Payload option for use only with JWSs. It may not be used with
> with unprotected JWTs or encrypted JWEs.
<a name="jws-unencoded-detached"></a>
#### Detached Payload Example
This example shows creating and parsing a compact JWS using an unencoded payload that is detached, i.e. where the
payload is not embedded in the compact JWS string at all.
We need to do three things during creation:
1. Specify the JWS signing key; it's a JWS and still needs to be signed.
2. Specify the raw payload bytes via the `JwtBuilder`'s `content` method.
3. Indicate that the payload should _not_ be Base64Url-encoded using the `JwtBuilder`'s `encodePayload(false)` method.

```java
// create a test key for this example:
SecretKey testKey = Jwts.SIG.HS512.key().build();

String message = "Hello World. It's a Beautiful Day!";
byte[] content = message.getBytes(StandardCharsets.UTF_8);

String jws = Jwts.builder().signWith(testKey) // #1
.content(content) // #2
.encodePayload(false) // #3
.compact();
```

To parse the resulting `jws` string, we need to do three things when creating the `JwtParser`:

1. Specify the signature verification key.
2. Indicate that we want to support Unencoded Payload Option JWSs by enabling the `b64` `crit` header parameter.
3. Specify the externally-transmitted unencoded payload bytes, required for signature verification.

```java
Jws<byte[]> parsed = Jwts.parser().verifyWith(testKey) // 1
.critical("b64") // 2
.build()
.parseContentJws(jws, content); // 3

assertArrayEquals(content, parsed.getPayload());
```

> **Note**
>
> **Disabled by Default**: Because of the aforementioned
> [security considerations](https://www.rfc-editor.org/rfc/rfc7797.html#section-8), Unencoded Payload Option
> JWSs are rejected by the parser by default. Simply enabling the `b64` `crit`ical header as shown above (#2) enables
> the feature, with the presumption that the application developer understands the security considerations when doing
> so.

<a name="jws-unencoded-nondetached"></a>
#### Non-Detached Payload Example

This example shows creating and parsing a compact JWS with what the RFC calls a 'non-detached' unencoded payload, i.e.
a raw string directly embedded as the payload in the compact JWS string.

We need to do three things during creation:

1. Specify the JWS signing key; it's a JWS and still needs to be signed.
2. Specify the raw payload string via the `JwtBuilder`'s `content` method. Per
[the RFC](https://www.rfc-editor.org/rfc/rfc7797.html#section-5.2), the payload string **_MUST NOT contain any
period (`.`) characters_**.
3. Indicate that the payload should _not_ be Base64Url-encoded using the `JwtBuilder`'s `encodePayload(false)` method.
```java
// create a test key for this example:
SecretKey testKey = Jwts.SIG.HS512.key().build();
String claimsString = "{\"sub\":\"joe\",\"iss\":\"me\"}";
String jws = Jwts.builder().signWith(testKey) // #1
.content(claimsString) // #2
.encodePayload(false) // #3
.compact();
```
If you were to print the `jws` string, you'd see something like this:

```
eyJhbGciOiJIUzUxMiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19.{"sub":"joe","iss":"me"}.wkoxYEd//...etc...
```

See how the `claimsString` is embedded directly as the center `payload` token instead of a standard Base64URL value?
This is why no period (`.`) characters can exist in the payload. If they did, any standard JWT parser would see more
than two periods total, which is required for parsing standard JWSs.


To parse the resulting `jws` string, we need to do two things when creating the `JwtParser`:

1. Specify the signature verification key.
2. Indicate that we want to support Unencoded Payload Option JWSs by enabling the `b64` `crit` header parameter.

```java
Jws<Claims> parsed = Jwts.parser().verifyWith(testKey) // 1
.critical("b64") // 2
.build()
.parseClaimsJws(jws);

assert "joe".equals(parsed.getPayload().getSubject());
assert "me".equals(parsed.getPayload().getIssuer());
```

Did you notice we used the `.parseClaimsJws(String)` method instead of `.parseClaimsJws(String, byte[])`? This is
because the non-detached payload is already present and JJWT has what it needs for signature verification.

Even so, you could call `.parseClaimsJws(String, byte[])` if you wanted by using the string's UTF-8 bytes:
```java
parsed = Jwts.parser().verifyWith(testKey)
.critical("b64")
.build()
.parseClaimsJws(jws, claimsString.getBytes(StandardCharsets.UTF_8)); // <---
```
<a name="jwe"></a>
## Encrypted JWTs
Expand Down
11 changes: 11 additions & 0 deletions api/src/main/java/io/jsonwebtoken/JwsHeader.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,15 @@ public interface JwsHeader extends ProtectedHeader {
*/
@Deprecated
String CRITICAL = "crit";

/**
* Returns {@code true} if the payload is Base64Url-encoded per standard JWS rules, or {@code false} if the
* <a href="https://datatracker.ietf.org/doc/html/rfc7797">RFC 7797: JSON Web Signature (JWS) Unencoded Payload
* Option</a> has been specified.
*
* @return {@code true} if the payload is Base64Url-encoded per standard JWS rules, or {@code false} if the
* <a href="https://datatracker.ietf.org/doc/html/rfc7797">RFC 7797: JSON Web Signature (JWS) Unencoded Payload
* Option</a> has been specified.
*/
boolean isPayloadEncoded();
}
39 changes: 38 additions & 1 deletion api/src/main/java/io/jsonwebtoken/JwtBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,29 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
@Deprecated
JwtBuilder setPayload(String payload);

/**
* Sets the JWT payload to be the specified string's UTF-8 bytes.
*
* <p><b>Content Type Recommendation</b></p>
*
* <p>Unless you are confident that the JWT recipient will <em>always</em> know how to use the string
* without additional metadata, it is strongly recommended to use the {@link #content(byte[], String)} method
* instead of this one. That method ensures that a JWT recipient can inspect the {@code cty} header to know
* how to handle the content without ambiguity.</p>
*
* <p><b>Mutually Exclusive Claims and Content</b></p>
*
* <p>This method is mutually exclusive of the {@link #claim(String, Object)} and {@link #claims()}
* methods. Either {@code claims} or {@code content} method variants may be used, but not both. If you want the
* JWT payload to be JSON claims, use the {@link #claim(String, Object)} or {@link #claims()} methods instead.</p>
*
* @param content the content string to use for the JWT payload
* @return the builder for method chaining.
* @see #content(byte[], String)
* @since JJWT_RELEASE_VERSION
*/
JwtBuilder content(String content);

/**
* Sets the JWT payload to be the specified content byte array.
*
Expand Down Expand Up @@ -240,7 +263,7 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
* @param content the content byte array that will be the JWT payload. Cannot be null or empty.
* @param cty the content type (media type) identifier attributed to the byte array. Cannot be null or empty.
* @return the builder for method chaining.
* @throws IllegalArgumentException if either {@code payload} or {@code cty} are null or empty.
* @throws IllegalArgumentException if either {@code content} or {@code cty} are null or empty.
* @since JJWT_RELEASE_VERSION
*/
JwtBuilder content(byte[] content, String cty) throws IllegalArgumentException;
Expand Down Expand Up @@ -837,6 +860,20 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
*/
JwtBuilder encoder(Encoder<byte[], String> encoder);

/**
* Enables <a href="https://datatracker.ietf.org/doc/html/rfc7797">RFC 7797: JSON Web Signature (JWS)
* Unencoded Payload Option</a> if {@code false}, or standard JWT/JWS/JWE payload encoding otherwise. The default
* value is {@code true} per standard RFC behavior rules.
*
* <p>This value may only be {@code false} for JWSs (signed JWTs). It may not be used for standard
* (unprotected) JWTs or encrypted JWTs (JWEs). The builder will throw an exception during {@link #compact()} if
* {@code false} and a JWS is not being created.</p>
*
* @param b64 whether to Base64URL-encode the JWS payload
* @return the builder for method chaining.
*/
JwtBuilder encodePayload(boolean b64);

/**
* Performs Map-to-JSON serialization with the specified Serializer. This is used by the builder to convert
* JWT/JWS/JWE headers and claims Maps to JSON strings as required by the JWT specification.
Expand Down
4 changes: 4 additions & 0 deletions api/src/main/java/io/jsonwebtoken/JwtParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@ Jwt<Header, Claims> parseClaimsJwt(String jwt) throws ExpiredJwtException, Unsup
Jws<byte[]> parseContentJws(String jws) throws UnsupportedJwtException, MalformedJwtException, SignatureException,
SecurityException, IllegalArgumentException;

Jws<byte[]> parseContentJws(String jws, byte[] unencodedPayload);

Jws<Claims> parseClaimsJws(String jws, byte[] unencodedPayload);

/**
* Parses the specified compact serialized JWS string based on the builder's current configuration state and
* returns the resulting Claims JWS instance.
Expand Down
2 changes: 2 additions & 0 deletions api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ public interface JwtParserBuilder extends Builder<JwtParser> {
*/
JwtParserBuilder enableUnsecuredDecompression();

JwtParserBuilder critical(String crit);

/**
* Specifies the {@link ProtectedHeader} parameter names used in JWT extensions supported by the application. If
* the parser encounters a Protected JWT that {@link ProtectedHeader#getCritical() requires} extensions, and
Expand Down
26 changes: 26 additions & 0 deletions api/src/main/java/io/jsonwebtoken/lang/Assert.java
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,32 @@ public static <T> T isInstanceOf(Class<T> type, Object obj, String message) {
return type.cast(obj);
}

/**
* Asserts that the provided object is an instance of the provided class, throwing an
* {@link IllegalStateException} otherwise.
* <pre class="code">Assert.stateIsInstance(Foo.class, foo);</pre>
*
* @param type the type to check against
* @param <T> the object's expected type
* @param obj the object to check
* @param message a message which will be prepended to the message produced by
* the function itself, and which may be used to provide context. It should
* normally end in a ": " or ". " so that the function generate message looks
* ok when prepended to it.
* @return the non-null object IFF it is an instance of the specified {@code type}.
* @throws IllegalStateException if the object is not an instance of clazz
* @see Class#isInstance
*/
public static <T> T stateIsInstance(Class<T> type, Object obj, String message) {
notNull(type, "Type to check cannot be null.");
if (!type.isInstance(obj)) {
String msg = message + "Object of class [" + Objects.nullSafeClassName(obj) +
"] must be an instance of " + type;
throw new IllegalStateException(msg);
}
return type.cast(obj);
}

/**
* Assert that <code>superType.isAssignableFrom(subType)</code> is <code>true</code>.
* <pre class="code">Assert.isAssignable(Number.class, myClass);</pre>
Expand Down
Loading

0 comments on commit 34aa334

Please sign in to comment.