Merge remote-tracking branch 'origin/master' into 60-convenience_expiration_setter_which_takes_a_duration

This commit is contained in:
pveeckhout 2024-02-17 00:46:53 +01:00
commit 4a76b69d59
No known key found for this signature in database
GPG Key ID: C9D3A6E3FCE19AF7
59 changed files with 3324 additions and 1987 deletions

View File

@ -1,6 +1,7 @@
name: CI
on:
workflow_dispatch:
pull_request: # all pull requests
push:
branches:
@ -17,7 +18,7 @@ jobs:
runs-on: 'ubuntu-latest'
name: jdk-${{ matrix.java }}-oracle
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up JDK
uses: oracle-actions/setup-java@v1
with:
@ -42,9 +43,9 @@ jobs:
runs-on: 'ubuntu-latest'
name: jdk-${{ matrix.java }}-temurin
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java }}
distribution: 'temurin'
@ -72,9 +73,9 @@ jobs:
JDK_MAJOR_VERSION: ${{ matrix.java }}
name: jdk-${{ matrix.java }}-zulu
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java }}
distribution: 'zulu'
@ -99,11 +100,11 @@ jobs:
license-check:
runs-on: 'ubuntu-latest'
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0 # avoid license plugin history warnings (plus it needs full history)
- name: Set up JDK
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '8'
@ -120,9 +121,9 @@ jobs:
#needs: zulu # wait until others finish so a coverage failure doesn't cancel others accidentally
runs-on: 'ubuntu-latest'
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '8'

View File

@ -1,5 +1,103 @@
## Release Notes
### 0.12.5
This patch release:
* Ensures that builders' `NestedCollection` changes are applied to the collection immediately as mutation methods are called, no longer
requiring application developers to call `.and()` to 'commit' or apply a change. For example, prior to this release,
the following code did not apply changes:
```java
JwtBuilder builder = Jwts.builder();
builder.audience().add("an-audience"); // no .and() call
builder.compact(); // would not keep 'an-audience'
```
Now this code works as expected and all other `NestedCollection` instances like it apply changes immediately (e.g. when calling
`.add(value)`).
However, standard fluent builder chains are still recommended for readability when feasible, e.g.
```java
Jwts.builder()
.audience().add("an-audience").and() // allows fluent chaining
.subject("Joe")
// etc...
.compact()
```
See [Issue 916](https://github.com/jwtk/jjwt/issues/916).
### 0.12.4
This patch release includes various changes listed below.
#### Jackson Default Parsing Behavior
This release makes two behavioral changes to JJWT's default Jackson `ObjectMapper` parsing settings:
1. In the interest of having stronger standards to reject potentially malformed/malicious/accidental JSON that could
have undesirable effects on an application, JJWT's default `ObjectMapper `is now configured to explicitly reject/fail
parsing JSON (JWT headers and/or Claims) if/when that JSON contains duplicate JSON member names.
For example, now the following JSON, if parsed, would fail (be rejected) by default:
```json
{
"hello": "world",
"thisWillFail": 42,
"thisWillFail": "test"
}
```
Technically, the JWT RFCs _do allow_ duplicate named fields as long as the last parsed member is the one used
(see [JWS RFC 7515, Section 4](https://datatracker.ietf.org/doc/html/rfc7515#section-4)), so this is allowed.
However, because JWTs often reflect security concepts, it's usually better to be defensive and reject these
unexpected scenarios by default. The RFC later supports this position/preference in
[Section 10.12](https://datatracker.ietf.org/doc/html/rfc7515#section-10.12):
Ambiguous and potentially exploitable situations
could arise if the JSON parser used does not enforce the uniqueness
of member names or returns an unpredictable value for duplicate
member names.
Finally, this is just a default, and the RFC does indeed allow duplicate member names if the last value is used,
so applications that require duplicates to be allowed can simply configure their own `ObjectMapper` and use
that with JJWT instead of assuming this (new) JJWT default. See
[Issue #877](https://github.com/jwtk/jjwt/issues/877) for more.
2. If using JJWT's support to use Jackson to parse
[Custom Claim Types](https://github.com/jwtk/jjwt#json-jackson-custom-types) (for example, a Claim that should be
unmarshalled into a POJO), and the JSON for that POJO contained a member that is not represented in the specified
class, Jackson would fail parsing by default. Because POJOs and JSON data models can sometimes be out of sync
due to different class versions, the default behavior has been changed to ignore these unknown JSON members instead
of failing (i.e. the `ObjectMapper`'s `DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES` is now set to `false`)
by default.
Again, if you prefer the stricter behavior of rejecting JSON with extra or unknown properties, you can configure
`true` on your own `ObjectMapper` instance and use that instance with the `Jwts.parser()` builder.
#### Additional Changes
This release also:
* Fixes a thread-safety issue when using `java.util.ServiceLoader` to dynamically lookup/instantiate pluggable
implementations of JJWT interfaces (e.g. JSON parsers, etc). See
[Issue #873](https://github.com/jwtk/jjwt/issues/873) and its documented fix in
[PR #893](https://github.com/jwtk/jjwt/pull/892).
* Ensures Android environments and older `org.json` library usages can parse JSON from a `JwtBuilder`-provided
`java.io.Reader` instance. [Issue 882](https://github.com/jwtk/jjwt/issues/882).
* Ensures a single string `aud` (Audience) claim is retained (without converting it to a `Set`) when copying/applying a
source Claims instance to a destination Claims builder. [Issue 890](https://github.com/jwtk/jjwt/issues/890).
* Ensures P-256, P-384 and P-521 Elliptic Curve JWKs zero-pad their field element (`x`, `y`, and `d`) byte array values
if necessary before Base64Url-encoding per [RFC 7518](https://datatracker.ietf.org/doc/html/rfc7518), Sections
[6.2.1.2](https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.2),
[6.2.1.3](https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.3), and
[6.2.2.1](https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.2.1), respectively.
[Issue 901](https://github.com/jwtk/jjwt/issues/901).
* Ensures that Secret JWKs for HMAC-SHA algorithms with `k` sizes larger than the algorithm minimum can
be parsed/used as expected. See [Issue #905](https://github.com/jwtk/jjwt/issues/905)
* Ensures there is an upper bound (maximum) iterations enforced for PBES2 decryption to help mitigate potential DoS
attacks. Many thanks to Jingcheng Yang and Jianjun Chen from Sichuan University and Zhongguancun Lab for their
work on this. See [PR 911](https://github.com/jwtk/jjwt/pull/911).
* Fixes various typos in documentation and JavaDoc. Thanks to those contributing pull requests for these!
### 0.12.3
This patch release:

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,7 @@
<parent>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-root</artifactId>
<version>0.12.4-SNAPSHOT</version>
<version>0.12.6-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -96,6 +96,17 @@ public interface ClaimsMutator<T extends ClaimsMutator<T>> {
* <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.3"><code>aud</code></a> (audience) Claim
* set, quietly ignoring any null, empty, whitespace-only, or existing value already in the set.
*
* <p>When finished, the {@code audience} collection's {@link AudienceCollection#and() and()} method may be used
* to continue configuration. For example:</p>
* <blockquote><pre>
* Jwts.builder() // or Jwts.claims()
*
* .audience().add("anAudience")<b>.and() // return parent</b>
*
* .subject("Joe") // resume configuration...
* // etc...
* </pre></blockquote>
*
* @return the {@link AudienceCollection AudienceCollection} to use for {@code aud} configuration.
* @see AudienceCollection AudienceCollection
* @see AudienceCollection#single(String) AudienceCollection.single(String)
@ -221,6 +232,16 @@ public interface ClaimsMutator<T extends ClaimsMutator<T>> {
* A {@code NestedCollection} for setting {@link #audience()} values that also allows overriding the collection
* to be a {@link #single(String) single string value} for legacy JWT recipients if necessary.
*
* <p>Because this interface extends {@link NestedCollection}, the {@link #and()} method may be used to continue
* parent configuration. For example:</p>
* <blockquote><pre>
* Jwts.builder() // or Jwts.claims()
*
* .audience().add("anAudience")<b>.and() // return parent</b>
*
* .subject("Joe") // resume parent configuration...
* // etc...</pre></blockquote>
*
* @param <P> the type of ClaimsMutator to return for method chaining.
* @see #single(String)
* @since 0.12.0

View File

@ -101,12 +101,25 @@ public interface JwtParserBuilder extends Builder<JwtParser> {
* the parser encounters a Protected JWT that {@link ProtectedHeader#getCritical() requires} extensions, and
* those extensions' header names are not specified via this method, the parser will reject that JWT.
*
* <p>The collection's {@link Conjunctor#and() and()} method returns to the builder for continued parser
* configuration, for example:</p>
* <blockquote><pre>
* parserBuilder.critical().add("headerName")<b>.{@link Conjunctor#and() and()} // etc...</b></pre></blockquote>
*
* <p><b>Extension Behavior</b></p>
*
* <p>The {@code critical} collection only identifies header parameter names that are used in extensions supported
* by the application. <b>Application developers, <em>not JJWT</em>, MUST perform the associated extension behavior
* using the parsed JWT</b>.</p>
*
* <p><b>Continued Parser Configuration</b></p>
* <p>When finished, use the collection's
* {@link Conjunctor#and() and()} method to continue parser configuration, for example:
* <blockquote><pre>
* Jwts.parser()
* .critical().add("headerName").<b>{@link Conjunctor#and() and()} // return parent</b>
* // resume parser configuration...</pre></blockquote>
*
* @return the {@link NestedCollection} to use for {@code crit} configuration.
* @see ProtectedHeader#getCritical()
* @since 0.12.0
@ -557,7 +570,7 @@ public interface JwtParserBuilder extends Builder<JwtParser> {
* <p>The collection's {@link Conjunctor#and() and()} method returns to the builder for continued parser
* configuration, for example:</p>
* <blockquote><pre>
* parserBuilder.enc().add(anAeadAlgorithm).{@link Conjunctor#and() and()} // etc...</pre></blockquote>
* parserBuilder.enc().add(anAeadAlgorithm)<b>.{@link Conjunctor#and() and()} // etc...</b></pre></blockquote>
*
* <p><b>Standard Algorithms and Overrides</b></p>
*
@ -597,7 +610,7 @@ public interface JwtParserBuilder extends Builder<JwtParser> {
* <p>The collection's {@link Conjunctor#and() and()} method returns to the builder for continued parser
* configuration, for example:</p>
* <blockquote><pre>
* parserBuilder.key().add(aKeyAlgorithm).{@link Conjunctor#and() and()} // etc...</pre></blockquote>
* parserBuilder.key().add(aKeyAlgorithm)<b>.{@link Conjunctor#and() and()} // etc...</b></pre></blockquote>
*
* <p><b>Standard Algorithms and Overrides</b></p>
*
@ -639,7 +652,7 @@ public interface JwtParserBuilder extends Builder<JwtParser> {
* <p>The collection's {@link Conjunctor#and() and()} method returns to the builder for continued parser
* configuration, for example:</p>
* <blockquote><pre>
* parserBuilder.sig().add(aSignatureAlgorithm).{@link Conjunctor#and() and()} // etc...</pre></blockquote>
* parserBuilder.sig().add(aSignatureAlgorithm)<b>.{@link Conjunctor#and() and()} // etc...</b></pre></blockquote>
*
* <p><b>Standard Algorithms and Overrides</b></p>
*
@ -680,7 +693,7 @@ public interface JwtParserBuilder extends Builder<JwtParser> {
* <p>The collection's {@link Conjunctor#and() and()} method returns to the builder for continued parser
* configuration, for example:</p>
* <blockquote><pre>
* parserBuilder.zip().add(aCompressionAlgorithm).{@link Conjunctor#and() and()} // etc...</pre></blockquote>
* parserBuilder.zip().add(aCompressionAlgorithm)<b>.{@link Conjunctor#and() and()} // etc...</b></pre></blockquote>
*
* <p><b>Standard Algorithms and Overrides</b></p>
*

View File

@ -33,9 +33,11 @@ public interface ProtectedHeaderMutator<T extends ProtectedHeaderMutator<T>> ext
/**
* Configures names of header parameters used by JWT or JWA specification extensions that <em>MUST</em> be
* understood and supported by the JWT recipient. When finished, use the collection's
* {@link Conjunctor#and() and()} method to return to the header builder, for example:
* {@link Conjunctor#and() and()} method to continue header configuration, for example:
* <blockquote><pre>
* builder.critical().add("headerName").{@link Conjunctor#and() and()} // etc...</pre></blockquote>
* headerBuilder
* .critical().add("headerName").<b>{@link Conjunctor#and() and()} // return parent</b>
* // resume header configuration...</pre></blockquote>
*
* @return the {@link NestedCollection} to use for {@code crit} configuration.
* @see <a href="https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.11">JWS <code>crit</code> (Critical) Header Parameter</a>

View File

@ -17,7 +17,12 @@ package io.jsonwebtoken.lang;
/**
* A {@link CollectionMutator} that can return access to its parent via the {@link Conjunctor#and() and()} method for
* continued configuration.
* continued configuration. For example:
* <blockquote><pre>
* builder
* .aNestedCollection()// etc...
* <b>.and() // return parent</b>
* // resume parent configuration...</pre></blockquote>
*
* @param <E> the type of elements in the collection
* @param <P> the parent to return

View File

@ -109,9 +109,9 @@ public interface JwkBuilder<K extends Key, J extends Jwk<K>, T extends JwkBuilde
* the key is intended to be used. When finished, use the collection's {@link Conjunctor#and() and()} method to
* return to the JWK builder, for example:
* <blockquote><pre>
* jwkBuilder.operations().add(aKeyOperation).{@link Conjunctor#and() and()} // etc...</pre></blockquote>
* jwkBuilder.operations().add(aKeyOperation)<b>.{@link Conjunctor#and() and()} // etc...</b></pre></blockquote>
*
* <p>The {@code and()} method will throw an {@link IllegalArgumentException} if any of the specified
* <p>The {@code add()} method(s) will throw an {@link IllegalArgumentException} if any of the specified
* {@code KeyOperation}s are not permitted by the JWK's
* {@link #operationPolicy(KeyOperationPolicy) operationPolicy}. See that documentation for more
* information on security vulnerabilities when using the same key with multiple algorithms.</p>

View File

@ -21,7 +21,7 @@
<parent>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-root</artifactId>
<version>0.12.4-SNAPSHOT</version>
<version>0.12.6-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

View File

@ -21,7 +21,7 @@
<parent>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-root</artifactId>
<version>0.12.4-SNAPSHOT</version>
<version>0.12.6-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

View File

@ -72,14 +72,8 @@ public class JacksonDeserializer<T> extends AbstractDeserializer<T> {
* @param claimTypeMap The claim name-to-class map used to deserialize claims into the given type
*/
public JacksonDeserializer(Map<String, Class<?>> claimTypeMap) {
// DO NOT reuse JacksonSerializer.DEFAULT_OBJECT_MAPPER as this could result in sharing the custom deserializer
// between instances
this(new ObjectMapper());
Assert.notNull(claimTypeMap, "Claim type map cannot be null.");
// register a new Deserializer
SimpleModule module = new SimpleModule();
module.addDeserializer(Object.class, new MappedTypeDeserializer(Collections.unmodifiableMap(claimTypeMap)));
objectMapper.registerModule(module);
// DO NOT specify JacksonSerializer.DEFAULT_OBJECT_MAPPER here as that would modify the shared instance
this(JacksonSerializer.newObjectMapper(), claimTypeMap);
}
/**
@ -92,6 +86,46 @@ public class JacksonDeserializer<T> extends AbstractDeserializer<T> {
this(objectMapper, (Class<T>) Object.class);
}
/**
* Creates a new JacksonDeserializer where the values of the claims can be parsed into given types by registering
* a type-converting {@link com.fasterxml.jackson.databind.Module Module} on the specified {@link ObjectMapper}.
* A common usage example is to parse custom User object out of a claim, for example the claims:
* <pre>{@code
* {
* "issuer": "https://issuer.example.com",
* "user": {
* "firstName": "Jill",
* "lastName": "Coder"
* }
* }}</pre>
* Passing a map of {@code ["user": User.class]} to this constructor would result in the {@code user} claim being
* transformed to an instance of your custom {@code User} class, instead of the default of {@code Map}.
* <p>
* Because custom type parsing requires modifying the state of a Jackson {@code ObjectMapper}, this
* constructor modifies the specified {@code objectMapper} argument and customizes it to support the
* specified {@code claimTypeMap}.
* <p>
* If you do not want your {@code ObjectMapper} instance modified, but also want to support custom types for
* JWT {@code Claims}, you will need to first customize your {@code ObjectMapper} instance by registering
* your custom types separately and then use the {@link #JacksonDeserializer(ObjectMapper)} constructor instead
* (which does not modify the {@code objectMapper} argument).
*
* @param objectMapper the objectMapper to modify by registering a custom type-converting
* {@link com.fasterxml.jackson.databind.Module Module}
* @param claimTypeMap The claim name-to-class map used to deserialize claims into the given type
* @since 0.12.4
*/
//TODO: Make this public on a minor release
// (cannot do that on a point release as that would violate semver)
private JacksonDeserializer(ObjectMapper objectMapper, Map<String, Class<?>> claimTypeMap) {
this(objectMapper);
Assert.notNull(claimTypeMap, "Claim type map cannot be null.");
// register a new Deserializer on the ObjectMapper instance:
SimpleModule module = new SimpleModule();
module.addDeserializer(Object.class, new MappedTypeDeserializer(Collections.unmodifiableMap(claimTypeMap)));
objectMapper.registerModule(module);
}
private JacksonDeserializer(ObjectMapper objectMapper, Class<T> returnType) {
Assert.notNull(objectMapper, "ObjectMapper cannot be null.");
Assert.notNull(returnType, "Return type cannot be null.");

View File

@ -16,6 +16,8 @@
package io.jsonwebtoken.jackson.io;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
@ -41,7 +43,26 @@ public class JacksonSerializer<T> extends AbstractSerializer<T> {
MODULE = module;
}
static final ObjectMapper DEFAULT_OBJECT_MAPPER = new ObjectMapper().registerModule(MODULE);
static final ObjectMapper DEFAULT_OBJECT_MAPPER = newObjectMapper();
/**
* Creates and returns a new ObjectMapper with the {@code jjwt-jackson} module registered and
* {@code JsonParser.Feature.STRICT_DUPLICATE_DETECTION} enabled (set to true) and
* {@code DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES} disabled (set to false).
*
* @return a new ObjectMapper with the {@code jjwt-jackson} module registered and
* {@code JsonParser.Feature.STRICT_DUPLICATE_DETECTION} enabled (set to true) and
* {@code DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES} disabled (set to false).
*
* @since 0.12.4
*/
// package protected on purpose, do not expose to the public API
static ObjectMapper newObjectMapper() {
return new ObjectMapper()
.registerModule(MODULE)
.configure(JsonParser.Feature.STRICT_DUPLICATE_DETECTION, true) // https://github.com/jwtk/jjwt/issues/877
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // https://github.com/jwtk/jjwt/issues/893
}
protected final ObjectMapper objectMapper;

View File

@ -16,6 +16,7 @@
//file:noinspection GrDeprecatedAPIUsage
package io.jsonwebtoken.jackson.io
import com.fasterxml.jackson.core.JsonParseException
import com.fasterxml.jackson.databind.ObjectMapper
import io.jsonwebtoken.io.DeserializationException
import io.jsonwebtoken.io.Deserializer
@ -120,6 +121,90 @@ class JacksonDeserializerTest {
assertEquals expected, result
}
/**
* Asserts https://github.com/jwtk/jjwt/issues/877
* @since 0.12.4
*/
@Test
void testStrictDuplicateDetection() {
// 'bKey' is repeated twice:
String json = """
{
"aKey":"oneValue",
"bKey": 15,
"bKey": "hello"
}
"""
try {
new JacksonDeserializer<>().deserialize(new StringReader(json))
fail()
} catch (DeserializationException expected) {
String causeMsg = "Duplicate field 'bKey'\n at [Source: (StringReader); line: 5, column: 23]"
String msg = "Unable to deserialize: $causeMsg"
assertEquals msg, expected.getMessage()
assertTrue expected.getCause() instanceof JsonParseException
assertEquals causeMsg, expected.getCause().getMessage()
}
}
/**
* Asserts https://github.com/jwtk/jjwt/issues/893
*/
@Test
void testIgnoreUnknownPropertiesWhenDeserializeWithCustomObject() {
long currentTime = System.currentTimeMillis()
String json = """
{
"oneKey":"oneValue",
"custom": {
"stringValue": "s-value",
"intValue": "11",
"dateValue": ${currentTime},
"shortValue": 22,
"longValue": 33,
"byteValue": 15,
"byteArrayValue": "${base64('bytes')}",
"unknown": "unknown",
"nestedValue": {
"stringValue": "nested-value",
"intValue": "111",
"dateValue": ${currentTime + 1},
"shortValue": 222,
"longValue": 333,
"byteValue": 10,
"byteArrayValue": "${base64('bytes2')}",
"unknown": "unknown"
}
}
}
"""
CustomBean expectedCustomBean = new CustomBean()
.setByteArrayValue("bytes".getBytes("UTF-8"))
.setByteValue(0xF as byte)
.setDateValue(new Date(currentTime))
.setIntValue(11)
.setShortValue(22 as short)
.setLongValue(33L)
.setStringValue("s-value")
.setNestedValue(new CustomBean()
.setByteArrayValue("bytes2".getBytes("UTF-8"))
.setByteValue(0xA as byte)
.setDateValue(new Date(currentTime + 1))
.setIntValue(111)
.setShortValue(222 as short)
.setLongValue(333L)
.setStringValue("nested-value")
)
def expected = [oneKey: "oneValue", custom: expectedCustomBean]
def result = new JacksonDeserializer(Maps.of("custom", CustomBean).build())
.deserialize(new StringReader(json))
assertEquals expected, result
}
/**
* For: https://github.com/jwtk/jjwt/issues/564
*/

View File

@ -21,7 +21,7 @@
<parent>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-root</artifactId>
<version>0.12.4-SNAPSHOT</version>
<version>0.12.6-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

View File

@ -21,7 +21,7 @@
<parent>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-root</artifactId>
<version>0.12.4-SNAPSHOT</version>
<version>0.12.6-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -21,7 +21,7 @@
<parent>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-root</artifactId>
<version>0.12.4-SNAPSHOT</version>
<version>0.12.6-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -107,9 +107,8 @@ public class DefaultJweHeaderMutator<T extends JweHeaderMutator<T>>
public NestedCollection<String, T> critical() {
return new DefaultNestedCollection<String, T>(self(), this.DELEGATE.get(DefaultProtectedHeader.CRIT)) {
@Override
public T and() {
protected void changed() {
put(DefaultProtectedHeader.CRIT, Collections.asSet(getCollection()));
return super.and();
}
};
}

View File

@ -532,7 +532,7 @@ public class DefaultJwtBuilder implements JwtBuilder {
if (this.serializer == null) { // try to find one based on the services available
//noinspection unchecked
json(Services.loadFirst(Serializer.class));
json(Services.get(Serializer.class));
}
if (!Collections.isEmpty(claims)) { // normalize so we have one object to deal with:

View File

@ -220,9 +220,8 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder {
public NestedCollection<String, JwtParserBuilder> critical() {
return new DefaultNestedCollection<String, JwtParserBuilder>(this, this.critical) {
@Override
public JwtParserBuilder and() {
protected void changed() {
critical = Collections.asSet(getCollection());
return super.and();
}
};
}
@ -304,9 +303,8 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder {
public NestedCollection<CompressionAlgorithm, JwtParserBuilder> zip() {
return new DefaultNestedCollection<CompressionAlgorithm, JwtParserBuilder>(this, this.zipAlgs.values()) {
@Override
public JwtParserBuilder and() {
protected void changed() {
zipAlgs = new IdRegistry<>(StandardCompressionAlgorithms.NAME, getCollection());
return super.and();
}
};
}
@ -315,9 +313,8 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder {
public NestedCollection<AeadAlgorithm, JwtParserBuilder> enc() {
return new DefaultNestedCollection<AeadAlgorithm, JwtParserBuilder>(this, this.encAlgs.values()) {
@Override
public JwtParserBuilder and() {
public void changed() {
encAlgs = new IdRegistry<>(StandardEncryptionAlgorithms.NAME, getCollection());
return super.and();
}
};
}
@ -326,9 +323,8 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder {
public NestedCollection<SecureDigestAlgorithm<?, ?>, JwtParserBuilder> sig() {
return new DefaultNestedCollection<SecureDigestAlgorithm<?, ?>, JwtParserBuilder>(this, this.sigAlgs.values()) {
@Override
public JwtParserBuilder and() {
public void changed() {
sigAlgs = new IdRegistry<>(StandardSecureDigestAlgorithms.NAME, getCollection());
return super.and();
}
};
}
@ -337,9 +333,8 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder {
public NestedCollection<KeyAlgorithm<?, ?>, JwtParserBuilder> key() {
return new DefaultNestedCollection<KeyAlgorithm<?, ?>, JwtParserBuilder>(this, this.keyAlgs.values()) {
@Override
public JwtParserBuilder and() {
public void changed() {
keyAlgs = new IdRegistry<>(StandardKeyAlgorithms.NAME, getCollection());
return super.and();
}
};
}
@ -370,7 +365,7 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder {
if (this.deserializer == null) {
//noinspection unchecked
json(Services.loadFirst(Deserializer.class));
json(Services.get(Deserializer.class));
}
if (this.signingKeyResolver != null && this.signatureVerificationKey != null) {
String msg = "Both a 'signingKeyResolver and a 'verifyWith' key cannot be configured. " +

View File

@ -24,6 +24,7 @@ import io.jsonwebtoken.lang.MapMutator;
import io.jsonwebtoken.lang.Strings;
import java.util.Date;
import java.util.Map;
import java.util.Set;
/**
@ -46,6 +47,31 @@ public class DelegatingClaimsMutator<T extends MapMutator<String, Object, T> & C
return self();
}
@Override // override starting in 0.12.4
public Object put(String key, Object value) {
if (AUDIENCE_STRING.getId().equals(key)) { // https://github.com/jwtk/jjwt/issues/890
if (value instanceof String) {
Object existing = get(key);
//noinspection deprecation
audience().single((String) value);
return existing;
}
// otherwise ensure that the Parameter type is the RFC-default data type (JSON Array of Strings):
getAudience();
}
// otherwise retain expected behavior:
return super.put(key, value);
}
@Override // overridden starting in 0.12.4
public void putAll(Map<? extends String, ?> m) {
if (m == null) return;
for (Map.Entry<? extends String, ?> entry : m.entrySet()) {
String s = entry.getKey();
put(s, entry.getValue()); // ensure local put is called per https://github.com/jwtk/jjwt/issues/890
}
}
<F> F get(Parameter<F> param) {
return this.DELEGATE.get(param);
}
@ -104,12 +130,12 @@ public class DelegatingClaimsMutator<T extends MapMutator<String, Object, T> & C
@Override
public T single(String audience) {
return audienceSingle(audience);
// DO NOT call changed() here - we don't want to replace the value with a collection
}
@Override
public T and() {
protected void changed() {
put(DefaultClaims.AUDIENCE, Collections.asSet(getCollection()));
return super.and();
}
};
}

View File

@ -50,7 +50,7 @@ public abstract class AbstractParserBuilder<T, B extends ParserBuilder<T, B>> im
public final Parser<T> build() {
if (this.deserializer == null) {
//noinspection unchecked
this.deserializer = Services.loadFirst(Deserializer.class);
this.deserializer = Services.get(Deserializer.class);
}
return doBuild();
}

View File

@ -240,4 +240,25 @@ public final class Bytes {
}
}
}
/**
* Pads the front of the specified byte array with zeros if necessary, returning a new padded result, or the
* original array unmodified if padding isn't necessary. Padding is only performed if {@code length} is greater
* than {@code bytes.length}.
*
* @param bytes the byte array to pre-pad with zeros if necessary
* @param length the length of the required output array
* @return the potentially pre-padded byte array, or the existing {@code bytes} array if padding wasn't necessary.
* @since 0.12.4
*/
public static byte[] prepad(byte[] bytes, int length) {
Assert.notNull(bytes, "byte array cannot be null.");
Assert.gt(length, 0, "length must be positive (> 0).");
if (bytes.length < length) { // need to pad with leading zero(es):
byte[] padded = new byte[length];
System.arraycopy(bytes, 0, padded, length - bytes.length, bytes.length);
bytes = padded;
}
return bytes;
}
}

View File

@ -37,38 +37,52 @@ public class DefaultCollectionMutator<E, M extends CollectionMutator<E, M>> impl
return (M) this;
}
@Override
public M add(E e) {
if (Objects.isEmpty(e)) return self();
private boolean doAdd(E e) {
if (Objects.isEmpty(e)) return false;
if (e instanceof Identifiable && !Strings.hasText(((Identifiable) e).getId())) {
String msg = e.getClass() + " getId() value cannot be null or empty.";
throw new IllegalArgumentException(msg);
}
this.collection.remove(e);
this.collection.add(e);
return this.collection.add(e);
}
@Override
public M add(E e) {
if (doAdd(e)) changed();
return self();
}
@Override
public M remove(E e) {
this.collection.remove(e);
if (this.collection.remove(e)) changed();
return self();
}
@Override
public M add(Collection<? extends E> c) {
boolean changed = false;
for (E element : Collections.nullSafe(c)) {
add(element);
changed = doAdd(element) || changed;
}
if (changed) changed();
return self();
}
@Override
public M clear() {
boolean changed = !Collections.isEmpty(this.collection);
this.collection.clear();
if (changed) changed();
return self();
}
/**
* Callback for subclasses that wish to be notified if the internal collection has changed via builder mutation
* methods.
*/
protected void changed() {
}
protected Collection<E> getCollection() {
return Collections.immutable(this.collection);
}

View File

@ -15,25 +15,24 @@
*/
package io.jsonwebtoken.impl.lang;
import io.jsonwebtoken.lang.Arrays;
import io.jsonwebtoken.lang.Assert;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ServiceLoader;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import static io.jsonwebtoken.lang.Collections.arrayToList;
/**
* Helper class for loading services from the classpath, using a {@link ServiceLoader}. Decouples loading logic for
* better separation of concerns and testability.
*/
public final class Services {
private static ConcurrentMap<Class<?>, ServiceLoader<?>> SERVICE_CACHE = new ConcurrentHashMap<>();
private static final ConcurrentMap<Class<?>, Object> SERVICES = new ConcurrentHashMap<>();
private static final List<ClassLoaderAccessor> CLASS_LOADER_ACCESSORS = arrayToList(new ClassLoaderAccessor[] {
private static final List<ClassLoaderAccessor> CLASS_LOADER_ACCESSORS = Arrays.asList(new ClassLoaderAccessor[]{
new ClassLoaderAccessor() {
@Override
public ClassLoader getClassLoader() {
@ -54,86 +53,58 @@ public final class Services {
}
});
private Services() {}
/**
* Loads and instantiates all service implementation of the given SPI class and returns them as a List.
*
* @param spi The class of the Service Provider Interface
* @param <T> The type of the SPI
* @return An unmodifiable list with an instance of all available implementations of the SPI. No guarantee is given
* on the order of implementations, if more than one.
*/
public static <T> List<T> loadAll(Class<T> spi) {
Assert.notNull(spi, "Parameter 'spi' must not be null.");
ServiceLoader<T> serviceLoader = serviceLoader(spi);
if (serviceLoader != null) {
List<T> implementations = new ArrayList<>();
for (T implementation : serviceLoader) {
implementations.add(implementation);
}
return implementations;
}
throw new UnavailableImplementationException(spi);
private Services() {
}
/**
* Loads the first available implementation the given SPI class from the classpath. Uses the {@link ServiceLoader}
* to find implementations. When multiple implementations are available it will return the first one that it
* encounters. There is no guarantee with regard to ordering.
* Returns the first available implementation for the given SPI class, checking an internal thread-safe cache first,
* and, if not found, using a {@link ServiceLoader} to find implementations. When multiple implementations are
* available it will return the first one that it encounters. There is no guarantee with regard to ordering.
*
* @param spi The class of the Service Provider Interface
* @param <T> The type of the SPI
* @return A new instance of the service.
* @throws UnavailableImplementationException When no implementation the SPI is available on the classpath.
* @return The first available instance of the service.
* @throws UnavailableImplementationException When no implementation of the SPI class can be found.
* @since 0.12.4
*/
public static <T> T loadFirst(Class<T> spi) {
Assert.notNull(spi, "Parameter 'spi' must not be null.");
ServiceLoader<T> serviceLoader = serviceLoader(spi);
if (serviceLoader != null) {
return serviceLoader.iterator().next();
public static <T> T get(Class<T> spi) {
// TODO: JDK8, replace this find/putIfAbsent logic with ConcurrentMap.computeIfAbsent
T instance = findCached(spi);
if (instance == null) {
instance = loadFirst(spi); // throws UnavailableImplementationException if not found, which is what we want
SERVICES.putIfAbsent(spi, instance); // cache if not already cached
}
throw new UnavailableImplementationException(spi);
return instance;
}
/**
* Returns a ServiceLoader for <code>spi</code> class, checking multiple classloaders. The ServiceLoader
* will be cached if it contains at least one implementation of the <code>spi</code> class.<BR>
*
* <b>NOTE:</b> Only the first Serviceloader will be cached.
* @param spi The interface or abstract class representing the service loader.
* @return A service loader, or null if no implementations are found
* @param <T> The type of the SPI.
*/
private static <T> ServiceLoader<T> serviceLoader(Class<T> spi) {
// TODO: JDK8, replace this get/putIfAbsent logic with ConcurrentMap.computeIfAbsent
ServiceLoader<T> serviceLoader = (ServiceLoader<T>) SERVICE_CACHE.get(spi);
if (serviceLoader != null) {
return serviceLoader;
private static <T> T findCached(Class<T> spi) {
Assert.notNull(spi, "Service interface cannot be null.");
Object obj = SERVICES.get(spi);
if (obj != null) {
return Assert.isInstanceOf(spi, obj, "Unexpected cached service implementation type.");
}
for (ClassLoaderAccessor classLoaderAccessor : CLASS_LOADER_ACCESSORS) {
serviceLoader = ServiceLoader.load(spi, classLoaderAccessor.getClassLoader());
if (serviceLoader.iterator().hasNext()) {
SERVICE_CACHE.putIfAbsent(spi, serviceLoader);
return serviceLoader;
}
}
return null;
}
private static <T> T loadFirst(Class<T> spi) {
for (ClassLoaderAccessor accessor : CLASS_LOADER_ACCESSORS) {
ServiceLoader<T> loader = ServiceLoader.load(spi, accessor.getClassLoader());
Assert.stateNotNull(loader, "JDK ServiceLoader#load should never return null.");
Iterator<T> i = loader.iterator();
Assert.stateNotNull(i, "JDK ServiceLoader#iterator() should never return null.");
if (i.hasNext()) {
return i.next();
}
}
throw new UnavailableImplementationException(spi);
}
/**
* Clears internal cache of ServiceLoaders. This is useful when testing, or for applications that dynamically
* Clears internal cache of service singletons. This is useful when testing, or for applications that dynamically
* change classloaders.
*/
public static void reload() {
SERVICE_CACHE.clear();
SERVICES.clear();
}
private interface ClassLoaderAccessor {

View File

@ -15,6 +15,7 @@
*/
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.Bytes;
import io.jsonwebtoken.impl.lang.Converters;
import io.jsonwebtoken.impl.lang.Parameter;
import io.jsonwebtoken.io.Encoders;
@ -24,6 +25,7 @@ import io.jsonwebtoken.security.UnsupportedKeyException;
import java.math.BigInteger;
import java.security.Key;
import java.security.interfaces.ECKey;
import java.security.spec.EllipticCurve;
import java.util.Set;
abstract class AbstractEcJwkFactory<K extends Key & ECKey, J extends Jwk<K>> extends AbstractFamilyJwkFactory<K, J> {
@ -41,19 +43,16 @@ abstract class AbstractEcJwkFactory<K extends Key & ECKey, J extends Jwk<K>> ext
* https://tools.ietf.org/html/rfc7518#section-6.2.1.2 indicates that this algorithm logic is defined in
* http://www.secg.org/sec1-v2.pdf Section 2.3.5.
*
* @param fieldSize EC field size
* @param coordinate EC point coordinate (e.g. x or y)
* @param curve EllipticCurve
* @param coordinate EC point coordinate (e.g. x or y) on the {@code curve}
* @return A base64Url-encoded String representing the EC field element per the RFC format
*/
// Algorithm defined in http://www.secg.org/sec1-v2.pdf Section 2.3.5
static String toOctetString(int fieldSize, BigInteger coordinate) {
static String toOctetString(EllipticCurve curve, BigInteger coordinate) {
byte[] bytes = Converters.BIGINT_UBYTES.applyTo(coordinate);
int mlen = (int) Math.ceil(fieldSize / 8d);
if (mlen > bytes.length) {
byte[] m = new byte[mlen];
System.arraycopy(bytes, 0, m, mlen - bytes.length, bytes.length);
bytes = m;
}
int fieldSizeInBits = curve.getField().getFieldSize();
int mlen = Bytes.length(fieldSizeInBits);
bytes = Bytes.prepad(bytes, mlen);
return Encoders.BASE64URL.encode(bytes);
}

View File

@ -111,11 +111,10 @@ abstract class AbstractJwkBuilder<K extends Key, J extends Jwk<K>, T extends Jwk
public NestedCollection<KeyOperation, T> operations() {
return new DefaultNestedCollection<KeyOperation, T>(self(), this.DELEGATE.getOperations()) {
@Override
public T and() {
protected void changed() {
Collection<? extends KeyOperation> c = getCollection();
opsPolicy.validate(c);
DELEGATE.setOperations(c);
return super.and();
}
};
}

View File

@ -30,6 +30,7 @@ import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.SecureRandom;
@ -54,9 +55,27 @@ abstract class AesAlgorithm extends CryptoAlgorithm implements KeyBuilderSupplie
protected final int tagBitLength;
protected final boolean gcm;
/**
* Ensures {@code keyBitLength is a valid AES key length}
* @param keyBitLength the key length (in bits) to check
* @since 0.12.4
*/
static void assertKeyBitLength(int keyBitLength) {
if (keyBitLength == 128 || keyBitLength == 192 || keyBitLength == 256) return; // valid
String msg = "Invalid AES key length: " + Bytes.bitsMsg(keyBitLength) + ". AES only supports " +
"128, 192, or 256 bit keys.";
throw new IllegalArgumentException(msg);
}
static SecretKey keyFor(byte[] bytes) {
int bitlen = (int) Bytes.bitLength(bytes);
assertKeyBitLength(bitlen);
return new SecretKeySpec(bytes, KEY_ALG_NAME);
}
AesAlgorithm(String id, final String jcaTransformation, int keyBitLength) {
super(id, jcaTransformation);
Assert.isTrue(keyBitLength == 128 || keyBitLength == 192 || keyBitLength == 256, "Invalid AES key length: it must equal 128, 192, or 256.");
assertKeyBitLength(keyBitLength);
this.keyBitLength = keyBitLength;
this.gcm = jcaTransformation.startsWith("AES/GCM");
this.ivBitLength = jcaTransformation.equals("AESWrap") ? 0 : (this.gcm ? GCM_IV_SIZE : BLOCK_SIZE);

View File

@ -31,7 +31,10 @@ import static io.jsonwebtoken.impl.security.DefaultEcPublicJwk.equalsPublic;
class DefaultEcPrivateJwk extends AbstractPrivateJwk<ECPrivateKey, ECPublicKey, EcPublicJwk> implements EcPrivateJwk {
static final Parameter<BigInteger> D = Parameters.secretBigInt("d", "ECC Private Key");
static final Parameter<BigInteger> D = Parameters.bigInt("d", "ECC Private Key")
.setConverter(FieldElementConverter.B64URL_CONVERTER)
.setSecret(true) // important!
.build();
static final Set<Parameter<?>> PARAMS = Collections.concat(DefaultEcPublicJwk.PARAMS, D);
DefaultEcPrivateJwk(JwkContext<ECPrivateKey> ctx, EcPublicJwk pubJwk) {

View File

@ -30,9 +30,12 @@ import java.util.Set;
class DefaultEcPublicJwk extends AbstractPublicJwk<ECPublicKey> implements EcPublicJwk {
static final String TYPE_VALUE = "EC";
static final Parameter<String> CRV = Parameters.string("crv", "Curve");
static final Parameter<BigInteger> X = Parameters.bigInt("x", "X Coordinate").build();
static final Parameter<BigInteger> Y = Parameters.bigInt("y", "Y Coordinate").build();
static final Parameter<BigInteger> X = Parameters.bigInt("x", "X Coordinate")
.setConverter(FieldElementConverter.B64URL_CONVERTER).build();
static final Parameter<BigInteger> Y = Parameters.bigInt("y", "Y Coordinate")
.setConverter(FieldElementConverter.B64URL_CONVERTER).build();
static final Set<Parameter<?>> PARAMS = Collections.concat(AbstractAsymmetricJwk.PARAMS, CRV, X, Y);
// https://www.rfc-editor.org/rfc/rfc7638#section-3.2
@ -52,4 +55,5 @@ class DefaultEcPublicJwk extends AbstractPublicJwk<ECPublicKey> implements EcPub
protected boolean equals(PublicJwk<?> jwk) {
return jwk instanceof EcPublicJwk && equalsPublic(this, jwk);
}
}

View File

@ -35,7 +35,8 @@ import java.security.spec.InvalidKeySpecException;
class EcPrivateJwkFactory extends AbstractEcJwkFactory<ECPrivateKey, EcPrivateJwk> {
private static final String ECPUBKEY_ERR_MSG = "JwkContext publicKey must be an " + ECPublicKey.class.getName() + " instance.";
private static final String ECPUBKEY_ERR_MSG = "JwkContext publicKey must be an " + ECPublicKey.class.getName() +
" instance.";
private static final EcPublicJwkFactory PUB_FACTORY = EcPublicJwkFactory.INSTANCE;
@ -96,8 +97,7 @@ class EcPrivateJwkFactory extends AbstractEcJwkFactory<ECPrivateKey, EcPrivateJw
ctx.setId(pubJwk.getId());
}
int fieldSize = key.getParams().getCurve().getField().getFieldSize();
String d = toOctetString(fieldSize, key.getS());
String d = toOctetString(key.getParams().getCurve(), key.getS());
ctx.put(DefaultEcPrivateJwk.D.getId(), d);
return new DefaultEcPrivateJwk(ctx, pubJwk);

View File

@ -81,11 +81,10 @@ class EcPublicJwkFactory extends AbstractEcJwkFactory<ECPublicKey, EcPublicJwk>
ctx.put(DefaultEcPublicJwk.CRV.getId(), curveId);
int fieldSize = curve.getField().getFieldSize();
String x = toOctetString(fieldSize, point.getAffineX());
String x = toOctetString(curve, point.getAffineX());
ctx.put(DefaultEcPublicJwk.X.getId(), x);
String y = toOctetString(fieldSize, point.getAffineY());
String y = toOctetString(curve, point.getAffineY());
ctx.put(DefaultEcPublicJwk.Y.getId(), y);
return new DefaultEcPublicJwk(ctx);

View File

@ -0,0 +1,69 @@
/*
* Copyright © 2024 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.io.Codec;
import io.jsonwebtoken.impl.lang.Bytes;
import io.jsonwebtoken.impl.lang.Converter;
import io.jsonwebtoken.impl.lang.Converters;
import java.math.BigInteger;
/**
* Hotfix for <a href="https://github.com/jwtk/jjwt/issues/901">JJWT Issue 901</a>. This is currently hard-coded
* expecting field elements for NIST P-256, P-384, or P-521 curves. Ideally this should be refactored to work for
* <em>any</em> curve based on its field size, not just for these NIST curves. However, the
* {@link EcPublicJwkFactory} and {@link EcPrivateJwkFactory} implementations only work with JWA NIST curves,
* so this implementation is acceptable until (and if) different Weierstrass elliptic curves (ever) need to be
* supported.
*
* @since 0.12.4
*/
final class FieldElementConverter implements Converter<BigInteger, byte[]> {
static final FieldElementConverter INSTANCE = new FieldElementConverter();
static final Converter<BigInteger, Object> B64URL_CONVERTER = Converters.forEncoded(BigInteger.class,
Converters.compound(INSTANCE, Codec.BASE64URL));
private static int bytelen(ECCurve curve) {
return Bytes.length(curve.toParameterSpec().getCurve().getField().getFieldSize());
}
private static final int P256_BYTE_LEN = bytelen(ECCurve.P256);
private static final int P384_BYTE_LEN = bytelen(ECCurve.P384);
private static final int P521_BYTE_LEN = bytelen(ECCurve.P521);
@Override
public byte[] applyTo(BigInteger bigInteger) {
byte[] bytes = Converters.BIGINT_UBYTES.applyTo(bigInteger);
int len = bytes.length;
if (len == P256_BYTE_LEN || len == P384_BYTE_LEN || len == P521_BYTE_LEN) return bytes;
if (len < P256_BYTE_LEN) {
bytes = Bytes.prepad(bytes, P256_BYTE_LEN);
} else if (len < P384_BYTE_LEN) {
bytes = Bytes.prepad(bytes, P384_BYTE_LEN);
} else { // > P-384, so must be P-521:
bytes = Bytes.prepad(bytes, P521_BYTE_LEN);
}
return bytes;
}
@Override
public BigInteger applyFrom(byte[] bytes) {
return Converters.BIGINT_UBYTES.applyFrom(bytes);
}
}

View File

@ -32,7 +32,7 @@ public final class JwksBridge {
@SuppressWarnings({"unchecked", "unused"}) // used via reflection by io.jsonwebtoken.security.Jwks
public static String UNSAFE_JSON(Jwk<?> jwk) {
Serializer<Map<String, ?>> serializer = Services.loadFirst(Serializer.class);
Serializer<Map<String, ?>> serializer = Services.get(Serializer.class);
Assert.stateNotNull(serializer, "Serializer lookup failed. Ensure JSON impl .jar is in the runtime classpath.");
NamedSerializer ser = new NamedSerializer("JWK", serializer);
ByteArrayOutputStream out = new ByteArrayOutputStream(512);

View File

@ -16,9 +16,11 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.JweHeader;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.impl.DefaultJweHeader;
import io.jsonwebtoken.impl.lang.Bytes;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.impl.lang.Parameter;
import io.jsonwebtoken.impl.lang.ParameterReadable;
import io.jsonwebtoken.impl.lang.RequiredParameterReader;
import io.jsonwebtoken.lang.Assert;
@ -50,11 +52,13 @@ public class Pbes2HsAkwAlgorithm extends CryptoAlgorithm implements KeyAlgorithm
"[JWA RFC 7518, Section 4.8.1.2](https://www.rfc-editor.org/rfc/rfc7518.html#section-4.8.1.2) " +
"recommends password-based-encryption iterations be greater than or equal to " +
MIN_RECOMMENDED_ITERATIONS + ". Provided: ";
private static final double MAX_ITERATIONS_FACTOR = 2.5;
private final int HASH_BYTE_LENGTH;
private final int DERIVED_KEY_BIT_LENGTH;
private final byte[] SALT_PREFIX;
private final int DEFAULT_ITERATIONS;
private final int MAX_ITERATIONS;
private final KeyAlgorithm<SecretKey, SecretKey> wrapAlg;
private static byte[] toRfcSaltPrefix(byte[] bytes) {
@ -106,6 +110,7 @@ public class Pbes2HsAkwAlgorithm extends CryptoAlgorithm implements KeyAlgorithm
} else {
DEFAULT_ITERATIONS = DEFAULT_SHA256_ITERATIONS;
}
MAX_ITERATIONS = (int) (DEFAULT_ITERATIONS * MAX_ITERATIONS_FACTOR); // upper bound to help mitigate DoS attacks
// https://www.rfc-editor.org/rfc/rfc7518.html#section-4.8, 2nd paragraph, last sentence:
// "Their derived-key lengths respectively are 16, 24, and 32 octets." :
@ -184,7 +189,16 @@ public class Pbes2HsAkwAlgorithm extends CryptoAlgorithm implements KeyAlgorithm
final Password key = Assert.notNull(request.getKey(), "Decryption Password cannot be null.");
ParameterReadable reader = new RequiredParameterReader(header);
final byte[] inputSalt = reader.get(DefaultJweHeader.P2S);
final int iterations = reader.get(DefaultJweHeader.P2C);
Parameter<Integer> param = DefaultJweHeader.P2C;
final int iterations = reader.get(param);
if (iterations > MAX_ITERATIONS) {
String msg = "JWE Header " + param + " value " + iterations + " exceeds " + getId() + " maximum " +
"allowed value " + MAX_ITERATIONS + ". The larger value is rejected to help mitigate " +
"potential Denial of Service attacks.";
throw new UnsupportedJwtException(msg);
}
final byte[] rfcSalt = Bytes.concat(SALT_PREFIX, inputSalt);
final char[] password = key.toCharArray(); // password will be safely cleaned/zeroed in deriveKey next:
final SecretKey derivedKek = deriveKey(request, password, rfcSalt, iterations);

View File

@ -15,6 +15,7 @@
*/
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.Identifiable;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.impl.lang.Bytes;
import io.jsonwebtoken.impl.lang.ParameterReadable;
@ -22,11 +23,14 @@ import io.jsonwebtoken.impl.lang.RequiredParameterReader;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Strings;
import io.jsonwebtoken.security.AeadAlgorithm;
import io.jsonwebtoken.security.InvalidKeyException;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.MacAlgorithm;
import io.jsonwebtoken.security.MalformedKeyException;
import io.jsonwebtoken.security.SecretJwk;
import io.jsonwebtoken.security.SecureDigestAlgorithm;
import io.jsonwebtoken.security.SecretKeyAlgorithm;
import io.jsonwebtoken.security.WeakKeyException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
@ -44,61 +48,97 @@ class SecretJwkFactory extends AbstractFamilyJwkFactory<SecretKey, SecretJwk> {
protected SecretJwk createJwkFromKey(JwkContext<SecretKey> ctx) {
SecretKey key = Assert.notNull(ctx.getKey(), "JwkContext key cannot be null.");
String k;
byte[] encoded = null;
try {
byte[] encoded = KeysBridge.getEncoded(key);
encoded = KeysBridge.getEncoded(key);
k = Encoders.BASE64URL.encode(encoded);
Assert.hasText(k, "k value cannot be null or empty.");
} catch (Throwable t) {
String msg = "Unable to encode SecretKey to JWK: " + t.getMessage();
throw new InvalidKeyException(msg, t);
} finally {
Bytes.clear(encoded);
}
MacAlgorithm mac = DefaultMacAlgorithm.findByKey(key);
if (mac != null) {
ctx.put(AbstractJwk.ALG.getId(), mac.getId());
}
ctx.put(DefaultSecretJwk.K.getId(), k);
return new DefaultSecretJwk(ctx);
return createJwkFromValues(ctx);
}
private static void assertKeyBitLength(byte[] bytes, MacAlgorithm alg) {
long bitLen = Bytes.bitLength(bytes);
long requiredBitLen = alg.getKeyBitLength();
if (bitLen != requiredBitLen) {
if (bitLen < requiredBitLen) {
// Implementors note: Don't print out any information about the `bytes` value itself - size,
// content, etc., as it is considered secret material:
String msg = "Secret JWK " + AbstractJwk.ALG + " value is '" + alg.getId() +
"', but the " + DefaultSecretJwk.K + " length does not equal the '" + alg.getId() +
"' length requirement of " + Bytes.bitsMsg(requiredBitLen) +
". This discrepancy could be the result of an algorithm " +
"substitution attack or simply an erroneously constructed JWK. In either case, it is likely " +
"to result in unexpected or undesired security consequences.";
throw new MalformedKeyException(msg);
"', but the " + DefaultSecretJwk.K + " length is smaller than the " + alg.getId() +
" minimum length of " + Bytes.bitsMsg(requiredBitLen) +
" required by " +
"[JWA RFC 7518, Section 3.2](https://www.rfc-editor.org/rfc/rfc7518.html#section-3.2), " +
"2nd paragraph: 'A key of the same size as the hash output or larger MUST be used with this " +
"algorithm.'";
throw new WeakKeyException(msg);
}
}
private static void assertSymmetric(Identifiable alg) {
if (alg instanceof MacAlgorithm || alg instanceof SecretKeyAlgorithm || alg instanceof AeadAlgorithm)
return; // valid
String msg = "Invalid Secret JWK " + AbstractJwk.ALG + " value '" + alg.getId() + "'. Secret JWKs " +
"may only be used with symmetric (secret) key algorithms.";
throw new MalformedKeyException(msg);
}
@Override
protected SecretJwk createJwkFromValues(JwkContext<SecretKey> ctx) {
ParameterReadable reader = new RequiredParameterReader(ctx);
byte[] bytes = reader.get(DefaultSecretJwk.K);
String jcaName = null;
final byte[] bytes = reader.get(DefaultSecretJwk.K);
SecretKey key;
String id = ctx.getAlgorithm();
if (Strings.hasText(id)) {
SecureDigestAlgorithm<?, ?> alg = Jwts.SIG.get().get(id);
if (alg instanceof MacAlgorithm) {
jcaName = ((CryptoAlgorithm) alg).getJcaName(); // valid for all JJWT alg implementations
Assert.hasText(jcaName, "Algorithm jcaName cannot be null or empty.");
assertKeyBitLength(bytes, (MacAlgorithm) alg);
}
}
if (!Strings.hasText(jcaName)) {
if (ctx.isSigUse()) {
String algId = ctx.getAlgorithm();
if (!Strings.hasText(algId)) { // optional per https://www.rfc-editor.org/rfc/rfc7517.html#section-4.4
// Here we try to infer the best type of key to create based on siguse and/or key length.
//
// AES requires 128, 192, or 256 bits, so anything larger than 256 cannot be AES, so we'll need to assume
// HMAC.
//
// Also, 256 bits works for either HMAC or AES, so we just have to choose one as there is no other
// RFC-based criteria for determining. Historically, we've chosen AES due to the larger number of
// KeyAlgorithm and AeadAlgorithm use cases, so that's our default.
int kBitLen = (int) Bytes.bitLength(bytes);
if (ctx.isSigUse() || kBitLen > Jwts.SIG.HS256.getKeyBitLength()) {
// The only JWA SecretKey signature algorithms are HS256, HS384, HS512, so choose based on bit length:
jcaName = "HmacSHA" + Bytes.bitLength(bytes);
} else { // not an HS* algorithm, and all standard AeadAlgorithms use AES keys:
jcaName = AesAlgorithm.KEY_ALG_NAME;
key = Keys.hmacShaKeyFor(bytes);
} else {
key = AesAlgorithm.keyFor(bytes);
}
ctx.setKey(key);
return new DefaultSecretJwk(ctx);
}
//otherwise 'alg' was specified, ensure it's valid for secret key use:
Identifiable alg = Jwts.SIG.get().get(algId);
if (alg == null) alg = Jwts.KEY.get().get(algId);
if (alg == null) alg = Jwts.ENC.get().get(algId);
if (alg != null) assertSymmetric(alg); // if we found a standard alg, it must be a symmetric key algorithm
if (alg instanceof MacAlgorithm) {
assertKeyBitLength(bytes, ((MacAlgorithm) alg));
String jcaName = ((CryptoAlgorithm) alg).getJcaName();
Assert.hasText(jcaName, "Algorithm jcaName cannot be null or empty.");
key = new SecretKeySpec(bytes, jcaName);
} else {
// all other remaining JWA-standard symmetric algs use AES:
key = AesAlgorithm.keyFor(bytes);
}
Assert.stateNotNull(jcaName, "jcaName cannot be null (invariant)");
SecretKey key = new SecretKeySpec(bytes, jcaName);
ctx.setKey(key);
return new DefaultSecretJwk(ctx);
}

View File

@ -23,6 +23,7 @@ import io.jsonwebtoken.impl.lang.Bytes
import io.jsonwebtoken.impl.lang.Services
import io.jsonwebtoken.impl.security.*
import io.jsonwebtoken.io.Decoders
import io.jsonwebtoken.io.Deserializer
import io.jsonwebtoken.io.Encoders
import io.jsonwebtoken.io.Serializer
import io.jsonwebtoken.lang.Strings
@ -74,7 +75,7 @@ class JwtsTest {
}
static def toJson(def o) {
def serializer = Services.loadFirst(Serializer)
def serializer = Services.get(Serializer)
def out = new ByteArrayOutputStream()
serializer.serialize(o, out)
return Strings.utf8(out.toByteArray())
@ -1167,6 +1168,36 @@ class JwtsTest {
.build().parseSignedClaims(jws)
}
/**
* Asserts that if a {@link Jwts#claims()} builder is used to set a single string Audience value, and the
* resulting constructed {@link Claims} instance is used on a {@link Jwts#builder()}, that the resulting JWT
* retains a single-string Audience value (and it is not automatically coerced to a {@code Set<String>}).
*
* @since 0.12.4
* @see <a href="https://github.com/jwtk/jjwt/issues/890">JJWT Issue 890</a>
*/
@Test
void testClaimsBuilderSingleStringAudienceThenJwtBuilder() {
def key = TestKeys.HS256
def aud = 'foo'
def claims = Jwts.claims().audience().single(aud).build()
def jws = Jwts.builder().claims(claims).signWith(key).compact()
// we can't use a JwtParser here because that will automatically normalize a single String value as a
// Set<String> for app developer convenience. So we assert that the JWT looks as expected by simple
// json parsing and map inspection
int i = jws.indexOf('.')
int j = jws.lastIndexOf('.')
def b64 = jws.substring(i, j)
def json = Strings.utf8(Decoders.BASE64URL.decode(b64))
def deser = Services.get(Deserializer)
def m = deser.deserialize(new StringReader(json)) as Map<String,?>
assertEquals aud, m.get('aud') // single string value
}
//Asserts correct/expected behavior discussed in https://github.com/jwtk/jjwt/issues/20
@Test
void testForgedTokenWithSwappedHeaderUsingNoneAlgorithm() {

View File

@ -29,8 +29,8 @@ import static org.junit.Assert.fail
class RFC7515AppendixETest {
static final Serializer<Map<String, ?>> serializer = Services.loadFirst(Serializer)
static final Deserializer<Map<String, ?>> deserializer = Services.loadFirst(Deserializer)
static final Serializer<Map<String, ?>> serializer = Services.get(Serializer)
static final Deserializer<Map<String, ?>> deserializer = Services.get(Deserializer)
static byte[] ser(def value) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(512)

View File

@ -100,11 +100,9 @@ class RFC7797Test {
def claims = Jwts.claims().subject('me').build()
ByteArrayOutputStream out = new ByteArrayOutputStream()
Services.loadFirst(Serializer).serialize(claims, out)
Services.get(Serializer).serialize(claims, out)
byte[] content = out.toByteArray()
//byte[] content = Services.loadFirst(Serializer).serialize(claims)
String s = Jwts.builder().signWith(key).content(content).encodePayload(false).compact()
// But verify with 3 types of sources: string, byte array, and two different kinds of InputStreams:

View File

@ -45,7 +45,7 @@ class DefaultJwtBuilderTest {
private DefaultJwtBuilder builder
private static byte[] serialize(Map<String, ?> map) {
def serializer = Services.loadFirst(Serializer)
def serializer = Services.get(Serializer)
ByteArrayOutputStream out = new ByteArrayOutputStream(512)
serializer.serialize(map, out)
return out.toByteArray()
@ -53,7 +53,7 @@ class DefaultJwtBuilderTest {
private static Map<String, ?> deser(byte[] data) {
def reader = Streams.reader(data)
Map<String, ?> m = Services.loadFirst(Deserializer).deserialize(reader) as Map<String, ?>
Map<String, ?> m = Services.get(Deserializer).deserialize(reader) as Map<String, ?>
return m
}
@ -749,7 +749,7 @@ class DefaultJwtBuilderTest {
// so we need to check the raw payload:
def encoded = new JwtTokenizer().tokenize(Streams.reader(jwt)).getPayload()
byte[] bytes = Decoders.BASE64URL.decode(encoded)
def claims = Services.loadFirst(Deserializer).deserialize(Streams.reader(bytes))
def claims = Services.get(Deserializer).deserialize(Streams.reader(bytes))
assertEquals two, claims.aud
}
@ -791,4 +791,47 @@ class DefaultJwtBuilderTest {
assertEquals three, claims.aud
}
/**
* Asserts that if a .audience() builder is used, and its .and() method is not called, the change to the
* audience is still applied when building the JWT.
* @see <a href="https://github.com/jwtk/jjwt/issues/916">JJWT Issue 916</a>
* @since 0.12.5
*/
@Test
void testAudienceWithoutConjunction() {
def aud = 'my-web'
def builder = Jwts.builder()
builder.audience().add(aud) // no .and() call
def jwt = builder.compact()
// assert that the resulting claims has the audience array set as expected:
def parsed = Jwts.parser().unsecured().build().parseUnsecuredClaims(jwt)
assertEquals aud, parsed.payload.getAudience()[0]
}
/**
* Asserts that if a .header().critical() builder is used, and its .and() method is not called, the change to the
* crit collection is still applied when building the header.
* @see <a href="https://github.com/jwtk/jjwt/issues/916">JJWT Issue 916</a>
* @since 0.12.5
*/
@Test
void testCritWithoutConjunction() {
def crit = 'test'
def builder = Jwts.builder().issuer('me')
def headerBuilder = builder.header()
headerBuilder.critical().add(crit) // no .and() method
headerBuilder.add(crit, 'foo') // no .and() method
builder.signWith(TestKeys.HS256)
def jwt = builder.compact()
def headerBytes = Decoders.BASE64URL.decode(jwt.split('\\.')[0])
def headerMap = Services.get(Deserializer).deserialize(Streams.reader(headerBytes)) as Map<String, ?>
def expected = [crit] as Set<String>
def val = headerMap.get('crit') as Set<String>
assertNotNull val
assertEquals expected, val
}
}

View File

@ -507,6 +507,22 @@ class DefaultJwtHeaderBuilderTest {
assertEquals expected, header.getCritical()
}
/**
* Asserts that if a .critical() builder is used, and its .and() method is not called, the change to the
* crit collection is still applied when building the header.
* @see <a href="https://github.com/jwtk/jjwt/issues/916">JJWT Issue 916</a>
* @since 0.12.5
*/
@Test
void testCritWithoutConjunction() {
def crit = 'test'
def builder = jws()
builder.add(crit, 'foo').critical().add(crit) // no .and() method
def header = builder.build() as ProtectedHeader
def expected = [crit] as Set<String>
assertEquals expected, header.getCritical()
}
@Test
void testCritSingleNullIgnored() {
def crit = 'test'

View File

@ -48,6 +48,22 @@ class DefaultJwtParserBuilderTest {
assertTrue builder.@critical.isEmpty()
}
/**
* Asserts that if a .critical() builder is used, and its .and() method is not called, the change to the
* crit collection is still applied when building the parser.
* @see <a href="https://github.com/jwtk/jjwt/issues/916">JJWT Issue 916</a>
* @since 0.12.5
*/
@Test
void testCriticalWithoutConjunction() {
builder.critical().add('foo') // no .and() call
assertFalse builder.@critical.isEmpty()
assertTrue builder.@critical.contains('foo')
def parser = builder.build()
assertFalse parser.@critical.isEmpty()
assertTrue parser.@critical.contains('foo')
}
@Test
void testSetProvider() {
Provider provider = createMock(Provider)
@ -173,6 +189,21 @@ class DefaultJwtParserBuilderTest {
assertSame codec, parser.zipAlgs.locate(header)
}
/**
* Asserts that if a .zip() builder is used, and its .and() method is not called, the change to the
* compression algorithm collection is still applied when building the parser.
* @see <a href="https://github.com/jwtk/jjwt/issues/916">JJWT Issue 916</a>
* @since 0.12.5
*/
@Test
void testAddCompressionAlgorithmWithoutConjunction() {
def codec = new TestCompressionCodec(id: 'test')
builder.zip().add(codec) // no .and() call
def parser = builder.build()
def header = Jwts.header().add('zip', codec.getId()).build()
assertSame codec, parser.zipAlgs.locate(header)
}
@Test
void testAddCompressionAlgorithmsOverrideDefaults() {
def header = Jwts.header().add('zip', 'DEF').build()
@ -211,6 +242,21 @@ class DefaultJwtParserBuilderTest {
assertSame custom, parser.encAlgs.apply(header) // custom one, not standard impl
}
/**
* Asserts that if an .enc() builder is used, and its .and() method is not called, the change to the
* encryption algorithm collection is still applied when building the parser.
* @see <a href="https://github.com/jwtk/jjwt/issues/916">JJWT Issue 916</a>
* @since 0.12.5
*/
@Test
void testAddEncryptionAlgorithmWithoutConjunction() {
def alg = new TestAeadAlgorithm(id: 'test')
builder.enc().add(alg) // no .and() call
def parser = builder.build() as DefaultJwtParser
def header = Jwts.header().add('alg', 'foo').add('enc', alg.getId()).build() as JweHeader
assertSame alg, parser.encAlgs.apply(header)
}
@Test
void testCaseSensitiveEncryptionAlgorithm() {
def alg = Jwts.ENC.A256GCM
@ -239,6 +285,23 @@ class DefaultJwtParserBuilderTest {
assertSame custom, parser.keyAlgs.apply(header) // custom one, not standard impl
}
/**
* Asserts that if an .key() builder is used, and its .and() method is not called, the change to the
* key algorithm collection is still applied when building the parser.
* @see <a href="https://github.com/jwtk/jjwt/issues/916">JJWT Issue 916</a>
* @since 0.12.5
*/
@Test
void testAddKeyAlgorithmWithoutConjunction() {
def alg = new TestKeyAlgorithm(id: 'test')
builder.key().add(alg) // no .and() call
def parser = builder.build() as DefaultJwtParser
def header = Jwts.header()
.add('enc', 'foo')
.add('alg', alg.getId()).build() as JweHeader
assertSame alg, parser.keyAlgs.apply(header)
}
@Test
void testCaseSensitiveKeyAlgorithm() {
def alg = Jwts.KEY.A256GCMKW
@ -268,6 +331,21 @@ class DefaultJwtParserBuilderTest {
assertSame custom, parser.sigAlgs.apply(header) // custom one, not standard impl
}
/**
* Asserts that if an .sig() builder is used, and its .and() method is not called, the change to the
* signature algorithm collection is still applied when building the parser.
* @see <a href="https://github.com/jwtk/jjwt/issues/916">JJWT Issue 916</a>
* @since 0.12.5
*/
@Test
void testAddSignatureAlgorithmWithoutConjunction() {
def alg = new TestMacAlgorithm(id: 'test')
builder.sig().add(alg) // no .and() call
def parser = builder.build() as DefaultJwtParser
def header = Jwts.header().add('alg', alg.getId()).build() as JwsHeader
assertSame alg, parser.sigAlgs.apply(header)
}
@Test
void testCaseSensitiveSignatureAlgorithm() {
def alg = Jwts.SIG.HS256

View File

@ -55,7 +55,7 @@ class DefaultJwtParserTest {
}
private static byte[] serialize(Map<String, ?> map) {
def serializer = Services.loadFirst(Serializer)
def serializer = Services.get(Serializer)
ByteArrayOutputStream out = new ByteArrayOutputStream(512)
serializer.serialize(map, out)
return out.toByteArray()

View File

@ -38,7 +38,7 @@ class RfcTests {
static final Map<String, ?> jsonToMap(String json) {
Reader r = new CharSequenceReader(json)
Map<String, ?> m = Services.loadFirst(Deserializer).deserialize(r) as Map<String, ?>
Map<String, ?> m = Services.get(Deserializer).deserialize(r) as Map<String, ?>
return m
}

View File

@ -27,11 +27,18 @@ import static org.junit.Assert.*
*/
class DefaultCollectionMutatorTest {
private int changeCount
private DefaultCollectionMutator m
@Before
void setUp() {
m = new DefaultCollectionMutator(null)
changeCount = 0
m = new DefaultCollectionMutator(null) {
@Override
protected void changed() {
changeCount++
}
}
}
@Test
@ -51,9 +58,17 @@ class DefaultCollectionMutatorTest {
void add() {
def val = 'hello'
m.add(val)
assertEquals 1, changeCount
assertEquals Collections.singleton(val), m.getCollection()
}
@Test
void addDuplicateDoesNotTriggerChange() {
m.add('hello')
m.add('hello') //already in the set, no change should be reflected
assertEquals 1, changeCount
}
@Test
void addCollection() {
def vals = ['hello', 'world']
@ -67,6 +82,17 @@ class DefaultCollectionMutatorTest {
assertFalse i.hasNext()
}
/**
* Asserts that if a collection is added, each internal addition to the collection doesn't call changed(); instead
* changed() is only called once after they've all been added to the collection
*/
@Test
void addCollectionTriggersSingleChange() {
def c = ['hello', 'world']
m.add(c)
assertEquals 1, changeCount // only one change triggered, not c.size()
}
@Test(expected = IllegalArgumentException)
void addIdentifiableWithNullId() {
def e = new Identifiable() {
@ -96,6 +122,12 @@ class DefaultCollectionMutatorTest {
assertEquals Collections.singleton('world'), m.getCollection()
}
@Test
void removeMissingDoesNotTriggerChange() {
m.remove('foo') // not in the collection, no change should be registered
assertEquals 0, changeCount
}
@Test
void clear() {
m.add('one').add('two').add(['three', 'four'])

View File

@ -20,32 +20,21 @@ import io.jsonwebtoken.impl.DefaultStubService
import org.junit.After
import org.junit.Test
import static org.junit.Assert.*
import static org.junit.Assert.assertEquals
import static org.junit.Assert.assertNotNull
class ServicesTest {
@Test
void testSuccessfulLoading() {
def factory = Services.loadFirst(StubService)
assertNotNull factory
assertEquals(DefaultStubService, factory.class)
def service = Services.get(StubService)
assertNotNull service
assertEquals(DefaultStubService, service.class)
}
@Test(expected = UnavailableImplementationException)
void testLoadFirstUnavailable() {
Services.loadFirst(NoService.class)
}
@Test
void testLoadAllAvailable() {
def list = Services.loadAll(StubService.class)
assertEquals 1, list.size()
assertTrue list[0] instanceof StubService
}
@Test(expected = UnavailableImplementationException)
void testLoadAllUnavailable() {
Services.loadAll(NoService.class)
void testLoadUnavailable() {
Services.get(NoService.class)
}
@Test

View File

@ -15,10 +15,16 @@
*/
package io.jsonwebtoken.impl.security
import io.jsonwebtoken.impl.lang.Bytes
import io.jsonwebtoken.impl.lang.Services
import io.jsonwebtoken.io.Decoders
import io.jsonwebtoken.io.Deserializer
import io.jsonwebtoken.security.Jwks
import io.jsonwebtoken.security.UnsupportedKeyException
import org.junit.Test
import java.security.interfaces.ECPrivateKey
import static org.junit.Assert.assertEquals
import static org.junit.Assert.fail
@ -35,4 +41,41 @@ class AbstractEcJwkFactoryTest {
assertEquals msg, e.getMessage()
}
}
/**
* Asserts correct behavior per https://github.com/jwtk/jjwt/issues/901
* @since 0.12.4
*/
@Test
void fieldElementByteArrayLength() {
EcSignatureAlgorithmTest.algs().each { alg ->
def key = alg.keyPair().build().getPrivate() as ECPrivateKey
def jwk = Jwks.builder().key(key).build()
def json = Jwks.UNSAFE_JSON(jwk)
def map = Services.get(Deserializer).deserialize(new StringReader(json)) as Map<String, ?>
def xs = map.get("x") as String
def ys = map.get("y") as String
def ds = map.get("d") as String
def x = Decoders.BASE64URL.decode(xs)
def y = Decoders.BASE64URL.decode(ys)
def d = Decoders.BASE64URL.decode(ds)
// most important part of the test: 'x' and 'y' decoded byte arrays must have a length equal to the curve
// field size (in bytes) per https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.2 and
// https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.3
int fieldSizeInBits = key.getParams().getCurve().getField().getFieldSize()
int fieldSizeInBytes = Bytes.length(fieldSizeInBits)
assertEquals fieldSizeInBytes, x.length
assertEquals fieldSizeInBytes, y.length
// and 'd' must have a length equal to the curve order size in bytes per
// https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.2.1
int orderSizeInBytes = Bytes.length(key.params.order.bitLength())
assertEquals orderSizeInBytes, d.length
}
}
}

View File

@ -29,10 +29,8 @@ import static org.junit.Assert.*
class AbstractJwkBuilderTest {
private static final SecretKey SKEY = TestKeys.A256GCM
private static AbstractJwkBuilder<SecretKey, SecretJwk, AbstractJwkBuilder> builder() {
return (AbstractJwkBuilder) Jwks.builder().key(SKEY)
return (AbstractJwkBuilder) Jwks.builder().key(TestKeys.NA256)
}
@Test
@ -241,7 +239,21 @@ class AbstractJwkBuilderTest {
.related(Jwks.OP.VERIFY.id).build()
def builder = builder().operationPolicy(Jwks.OP.policy().add(op).build())
def jwk = builder.operations().add(Collections.setOf(op, Jwks.OP.VERIFY)).and().build() as Jwk
assertSame op, jwk.getOperations().find({it.id == 'sign'})
assertSame op, jwk.getOperations().find({ it.id == 'sign' })
}
/**
* Asserts that if a .operations() builder is used, and its .and() method is not called, the change to the
* operations collection is still applied when building the JWK.
* @see <a href="https://github.com/jwtk/jjwt/issues/916">JJWT Issue 916</a>
* @since 0.12.5
*/
@Test
void testOperationsWithoutConjunction() {
def builder = builder()
builder.operations().clear().add(Jwks.OP.DERIVE_BITS) // no .and() call
def jwk = builder.build()
assertEquals(Jwks.OP.DERIVE_BITS, jwk.getOperations()[0])
}
@Test

View File

@ -109,9 +109,9 @@ class DispatchingJwkFactoryTest {
assertTrue jwk instanceof EcPrivateJwk
def key = jwk.toKey()
assertTrue key instanceof ECPrivateKey
String x = AbstractEcJwkFactory.toOctetString(key.params.curve.field.fieldSize, jwk.toPublicJwk().toKey().w.affineX)
String y = AbstractEcJwkFactory.toOctetString(key.params.curve.field.fieldSize, jwk.toPublicJwk().toKey().w.affineY)
String d = AbstractEcJwkFactory.toOctetString(key.params.curve.field.fieldSize, key.s)
String x = AbstractEcJwkFactory.toOctetString(key.params.curve, jwk.toPublicJwk().toKey().w.affineX)
String y = AbstractEcJwkFactory.toOctetString(key.params.curve, jwk.toPublicJwk().toKey().w.affineY)
String d = AbstractEcJwkFactory.toOctetString(key.params.curve, key.s)
assertEquals jwk.d.get(), d
//remove the 'd' mapping to represent only a public key:

View File

@ -0,0 +1,41 @@
/*
* Copyright © 2024 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.security
import io.jsonwebtoken.impl.lang.Bytes
import org.junit.Test
import static org.junit.Assert.assertEquals
/**
* @since 0.12.4
*/
class FieldElementConverterTest {
static FieldElementConverter converter = FieldElementConverter.INSTANCE
@Test
void p384CoordinateNeedsPadding() {
def requiredByteLen = 48
def coordBytes = Bytes.random(requiredByteLen - 1) // one less to see if padding is applied
def coord = new BigInteger(1, coordBytes)
byte[] result = converter.applyTo(coord)
assertEquals requiredByteLen, result.length
assertEquals 0x00 as byte, result[0]
//ensure roundtrip works:
assertEquals coord, converter.applyFrom(result)
}
}

View File

@ -97,7 +97,7 @@ class JwkSerializationTest {
static void testSecretJwk(Serializer ser, Deserializer des) {
def key = TestKeys.A128GCM
def key = TestKeys.NA256
def jwk = Jwks.builder().key(key).id('id').build()
assertWrapped(jwk, ['k'])

View File

@ -40,7 +40,7 @@ import static org.junit.Assert.*
class JwksTest {
private static final SecretKey SKEY = Jwts.SIG.HS256.key().build()
private static final SecretKey SKEY = TestKeys.NA256
private static final java.security.KeyPair EC_PAIR = Jwts.SIG.ES256.keyPair().build()
private static String srandom() {
@ -172,7 +172,7 @@ class JwksTest {
@Test
void testOperations() {
def val = [Jwks.OP.SIGN, Jwks.OP.VERIFY] as Set<KeyOperation>
def jwk = Jwks.builder().key(TestKeys.A128GCM).operations().add(val).and().build()
def jwk = Jwks.builder().key(TestKeys.NA256).operations().add(val).and().build()
assertEquals val, jwk.getOperations()
}

View File

@ -16,8 +16,11 @@
package io.jsonwebtoken.impl.security
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.UnsupportedJwtException
import io.jsonwebtoken.impl.DefaultJweHeaderMutator
import io.jsonwebtoken.impl.DefaultMutableJweHeader
import io.jsonwebtoken.io.Encoders
import io.jsonwebtoken.lang.Strings
import io.jsonwebtoken.security.KeyRequest
import io.jsonwebtoken.security.Keys
import io.jsonwebtoken.security.Password
@ -50,6 +53,39 @@ class Pbes2HsAkwAlgorithmTest {
}
}
/**
* @since 0.12.4
*/
@Test
void testExceedsMaxIterations() {
for (Pbes2HsAkwAlgorithm alg : ALGS) {
def password = Keys.password('correct horse battery staple'.toCharArray())
def iterations = alg.MAX_ITERATIONS + 1
// we make the JWE string directly from JSON here (instead of using Jwts.builder()) to avoid
// the computational time it would take to create such JWEs with excessive iterations as well as
// avoid the builder throwing any exceptions (and this is what a potential attacker would do anyway):
def headerJson = """
{
"p2c": ${iterations},
"p2s": "831BG_z_ZxkN7Rnt5v1iYm1A0bn6VEuxpW4gV7YBMoE",
"alg": "${alg.id}",
"enc": "A256GCM"
}"""
def jwe = Encoders.BASE64URL.encode(Strings.utf8(headerJson)) +
'.OSAhMk3FtaCeZ5v1c8bWBgssEVqx2mCPUEnJUsg4hwIQyrUP-LCYkg.' +
'K4R_-zb4qaZ3R0W8.sGS4mcT_xBhZC1d7G-g.kWqd_4sEsaKrWE_hMZ5HmQ'
try {
Jwts.parser().decryptWith(password).build().parse(jwe)
} catch (UnsupportedJwtException expected) {
String msg = "JWE Header 'p2c' (PBES2 Count) value ${iterations} exceeds ${alg.id} maximum allowed " +
"value ${alg.MAX_ITERATIONS}. The larger value is rejected to help mitigate potential " +
"Denial of Service attacks."
//println msg
assertEquals msg, expected.message
}
}
}
// for manual/developer testing only. Takes a long time and there is no deterministic output to assert
/*
@Test

View File

@ -23,6 +23,7 @@ import org.junit.Test
import java.security.interfaces.ECPrivateKey
import java.security.interfaces.RSAPrivateCrtKey
import java.security.spec.EllipticCurve
import static org.junit.Assert.*
@ -31,8 +32,8 @@ import static org.junit.Assert.*
*/
class RFC7517AppendixA2Test {
private static final String ecEncode(int fieldSize, BigInteger coord) {
return AbstractEcJwkFactory.toOctetString(fieldSize, coord)
private static final String ecEncode(EllipticCurve curve, BigInteger coord) {
return AbstractEcJwkFactory.toOctetString(curve, coord)
}
private static final String rsaEncode(BigInteger i) {
@ -90,17 +91,17 @@ class RFC7517AppendixA2Test {
def m = keys[0]
def jwk = Jwks.builder().add(m).build() as EcPrivateJwk
def key = jwk.toKey()
int fieldSize = key.params.curve.field.fieldSize
def curve = key.params.curve
assertTrue key instanceof ECPrivateKey
assertEquals m.size(), jwk.size()
assertEquals m.kty, jwk.getType()
assertEquals m.crv, jwk.get('crv')
assertEquals m.x, jwk.get('x')
assertEquals m.x, ecEncode(fieldSize, jwk.toPublicJwk().toKey().w.affineX)
assertEquals m.x, ecEncode(curve, jwk.toPublicJwk().toKey().w.affineX)
assertEquals m.y, jwk.get('y')
assertEquals m.y, ecEncode(fieldSize, jwk.toPublicJwk().toKey().w.affineY)
assertEquals m.y, ecEncode(curve, jwk.toPublicJwk().toKey().w.affineY)
assertEquals m.d, jwk.get('d').get()
assertEquals m.d, ecEncode(fieldSize, key.s)
assertEquals m.d, ecEncode(curve, key.s)
assertEquals m.use, jwk.getPublicKeyUse()
assertEquals m.kid, jwk.getId()

View File

@ -43,7 +43,7 @@ class RFC7518AppendixCTest {
}
private static final Map<String, ?> fromJson(String s) {
return Services.loadFirst(Deserializer).deserialize(new StringReader(s)) as Map<String, ?>
return Services.get(Deserializer).deserialize(new StringReader(s)) as Map<String, ?>
}
private static EcPrivateJwk readJwk(String json) {

View File

@ -15,9 +15,10 @@
*/
package io.jsonwebtoken.impl.security
import io.jsonwebtoken.security.Jwks
import io.jsonwebtoken.security.MalformedKeyException
import io.jsonwebtoken.security.SecretJwk
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.impl.lang.Bytes
import io.jsonwebtoken.io.Encoders
import io.jsonwebtoken.security.*
import org.junit.Test
import static org.junit.Assert.*
@ -30,10 +31,14 @@ import static org.junit.Assert.*
*/
class SecretJwkFactoryTest {
private static Set<MacAlgorithm> macAlgs() {
return Jwts.SIG.get().values().findAll({ it -> it instanceof MacAlgorithm }) as Collection<MacAlgorithm>
}
@Test
// if a jwk does not have an 'alg' or 'use' param, we default to an AES key
void testNoAlgNoSigJcaName() {
SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).delete('alg').build()
SecretJwk jwk = Jwks.builder().key(TestKeys.NA256).build()
SecretJwk result = Jwks.builder().add(jwk).build() as SecretJwk
assertEquals 'AES', result.toKey().getAlgorithm()
}
@ -47,7 +52,7 @@ class SecretJwkFactoryTest {
@Test
void testSignOpSetsKeyHmacSHA256() {
SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).delete('alg').build()
SecretJwk jwk = Jwks.builder().key(TestKeys.NA256).build()
SecretJwk result = Jwks.builder().add(jwk).operations().add(Jwks.OP.SIGN).and().build() as SecretJwk
assertNull result.getAlgorithm()
assertNull result.get('use')
@ -63,7 +68,7 @@ class SecretJwkFactoryTest {
@Test
void testSignOpSetsKeyHmacSHA384() {
SecretJwk jwk = Jwks.builder().key(TestKeys.HS384).delete('alg').build()
SecretJwk jwk = Jwks.builder().key(TestKeys.NA384).build()
SecretJwk result = Jwks.builder().add(jwk).operations().add(Jwks.OP.SIGN).and().build() as SecretJwk
assertNull result.getAlgorithm()
assertNull result.get('use')
@ -79,7 +84,7 @@ class SecretJwkFactoryTest {
@Test
void testSignOpSetsKeyHmacSHA512() {
SecretJwk jwk = Jwks.builder().key(TestKeys.HS512).delete('alg').build()
SecretJwk jwk = Jwks.builder().key(TestKeys.NA512).build()
SecretJwk result = Jwks.builder().add(jwk).operations().add(Jwks.OP.SIGN).and().build() as SecretJwk
assertNull result.getAlgorithm()
assertNull result.get('use')
@ -89,7 +94,7 @@ class SecretJwkFactoryTest {
@Test
// no 'alg' jwk property, but 'use' is 'sig', so forces jcaName to be HmacSHA256
void testNoAlgAndSigUseForHS256() {
SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).delete('alg').build()
SecretJwk jwk = Jwks.builder().key(TestKeys.NA256).build()
assertFalse jwk.containsKey('alg')
assertFalse jwk.containsKey('use')
SecretJwk result = Jwks.builder().add(jwk).add('use', 'sig').build() as SecretJwk
@ -99,7 +104,7 @@ class SecretJwkFactoryTest {
@Test
// no 'alg' jwk property, but 'use' is 'sig', so forces jcaName to be HmacSHA384
void testNoAlgAndSigUseForHS384() {
SecretJwk jwk = Jwks.builder().key(TestKeys.HS384).delete('alg').build()
SecretJwk jwk = Jwks.builder().key(TestKeys.NA384).build()
assertFalse jwk.containsKey('alg')
assertFalse jwk.containsKey('use')
SecretJwk result = Jwks.builder().add(jwk).add('use', 'sig').build() as SecretJwk
@ -109,7 +114,7 @@ class SecretJwkFactoryTest {
@Test
// no 'alg' jwk property, but 'use' is 'sig', so forces jcaName to be HmacSHA512
void testNoAlgAndSigUseForHS512() {
SecretJwk jwk = Jwks.builder().key(TestKeys.HS512).delete('alg').build()
SecretJwk jwk = Jwks.builder().key(TestKeys.NA512).build()
assertFalse jwk.containsKey('alg')
assertFalse jwk.containsKey('use')
SecretJwk result = Jwks.builder().add(jwk).add('use', 'sig').build() as SecretJwk
@ -119,20 +124,35 @@ class SecretJwkFactoryTest {
@Test
// no 'alg' jwk property, but 'use' is something other than 'sig', so jcaName should default to AES
void testNoAlgAndNonSigUse() {
SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).delete('alg').build()
SecretJwk jwk = Jwks.builder().key(TestKeys.NA256).build()
assertFalse jwk.containsKey('alg')
assertFalse jwk.containsKey('use')
SecretJwk result = Jwks.builder().add(jwk).add('use', 'foo').build() as SecretJwk
assertEquals 'AES', result.toKey().getAlgorithm()
}
/**
* @since 0.12.4
*/
@Test
// 'oct' type, but 'alg' value is not a secret key algorithm (and therefore malformed)
void testMismatchedAlgorithm() {
try {
Jwks.builder().key(TestKeys.NA256).add('alg', Jwts.SIG.RS256.getId()).build()
fail()
} catch (MalformedKeyException expected) {
String msg = "Invalid Secret JWK ${AbstractJwk.ALG} value 'RS256'. Secret JWKs may only be used with " +
"symmetric (secret) key algorithms."
assertEquals msg, expected.message
}
}
/**
* Test the case where a jwk `alg` value is present, but the key material doesn't match that algs key length
* requirements. This would be a malformed key.
*/
@Test
void testSizeMismatchedSecretJwk() {
//first get a valid HS256 JWK:
SecretJwk validJwk = Jwks.builder().key(TestKeys.HS256).build()
@ -142,12 +162,73 @@ class SecretJwkFactoryTest {
.add('alg', 'HS384')
.build()
fail()
} catch (MalformedKeyException expected) {
String msg = "Secret JWK 'alg' (Algorithm) value is 'HS384', but the 'k' (Key Value) length does " +
"not equal the 'HS384' length requirement of 384 bits (48 bytes). This discrepancy could " +
"be the result of an algorithm substitution attack or simply an erroneously constructed " +
"JWK. In either case, it is likely to result in unexpected or undesired security consequences."
} catch (WeakKeyException expected) {
String msg = "Secret JWK 'alg' (Algorithm) value is 'HS384', but the 'k' (Key Value) length is smaller " +
"than the HS384 minimum length of 384 bits (48 bytes) required by " +
"[JWA RFC 7518, Section 3.2](https://www.rfc-editor.org/rfc/rfc7518.html#section-3.2), 2nd " +
"paragraph: 'A key of the same size as the hash output or larger MUST be used with this " +
"algorithm.'"
assertEquals msg, expected.getMessage()
}
}
/**
* Test when a {@code k} size is smaller, equal to, and larger than the minimum required number of bits/bytes for
* a given HmacSHA* algorithm. The RFCs indicate smaller-than is not allowed, while equal-to and greater-than are
* allowed.
*
* This test asserts this allowed behavior per https://github.com/jwtk/jjwt/issues/905
* @see <a href="https://github.com/jwtk/jjwt/issues/905">JJWT Issue 905</a>
* @since 0.12.4
*/
@Test
void testAllowedKeyLengths() {
def parser = Jwks.parser().build()
for (MacAlgorithm alg : macAlgs()) {
// 3 key length sizes for each alg to test:
// index 0: smaller than minimum required
// index 1: minimum required
// index 2: more than minimum required:
def sizes = [alg.keyBitLength - Byte.SIZE, alg.keyBitLength, alg.keyBitLength + Byte.SIZE]
for (int i = 0; i < sizes.size(); i++) {
def kBitLength = sizes.get(i)
def k = Bytes.random(Bytes.length(kBitLength))
def jwkJson = """
{
"kid": "${UUID.randomUUID().toString()}",
"kty": "oct",
"alg": "${alg.getId()}",
"k": "${Encoders.BASE64URL.encode(k)}"
}""".toString()
def jwk
try {
jwk = parser.parse(jwkJson)
} catch (WeakKeyException expected) {
assertEquals("Should only occur on index 0 with less-than-minimum key length", 0, i)
String msg = "Secret JWK 'alg' (Algorithm) value is '${alg.getId()}', but the 'k' (Key Value) " +
"length is smaller than the ${alg.getId()} minimum length of " +
"${Bytes.bitsMsg(alg.keyBitLength)} required by " +
"[JWA RFC 7518, Section 3.2](https://www.rfc-editor.org/rfc/rfc7518.html#section-3.2), " +
"2nd paragraph: 'A key of the same size as the hash output or larger MUST be used with " +
"this algorithm.'"
assertEquals msg, expected.getMessage()
continue // expected for index 0 (purposefully weak key), so let loop continue
}
// otherwise not weak, sizes should reflect equal-to or greater-than alg bitlength sizes
assert jwk instanceof SecretJwk
assertEquals alg.getId(), jwk.getAlgorithm()
def bytes = jwk.toKey().getEncoded()
assertTrue Bytes.bitLength(bytes) >= alg.keyBitLength
assertEquals Bytes.length(kBitLength), jwk.toKey().getEncoded().length
}
}
}
}

View File

@ -21,6 +21,7 @@ import io.jsonwebtoken.lang.Collections
import io.jsonwebtoken.security.Jwks
import javax.crypto.SecretKey
import javax.crypto.spec.SecretKeySpec
import java.security.KeyPair
import java.security.PrivateKey
import java.security.Provider
@ -42,6 +43,11 @@ class TestKeys {
static SecretKey HS512 = Jwts.SIG.HS512.key().build()
static Collection<SecretKey> HS = Collections.setOf(HS256, HS384, HS512)
static SecretKey NA256 = new SecretKeySpec(HS256.encoded, "NONE")
static SecretKey NA384 = new SecretKeySpec(HS384.encoded, "NONE")
static SecretKey NA512 = new SecretKeySpec(HS512.encoded, "NONE")
static Collection<SecretKey> NA = [NA256, NA384, NA512]
static SecretKey A128GCM, A192GCM, A256GCM, A128KW, A192KW, A256KW, A128GCMKW, A192GCMKW, A256GCMKW
static Collection<SecretKey> AGCM
static {
@ -59,6 +65,7 @@ class TestKeys {
static Collection<SecretKey> SECRET = new LinkedHashSet<>()
static {
SECRET.addAll(HS)
SECRET.addAll(NA)
SECRET.addAll(AGCM)
SECRET.addAll(ACBC)
}

View File

@ -19,7 +19,7 @@
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-root</artifactId>
<version>0.12.4-SNAPSHOT</version>
<version>0.12.6-SNAPSHOT</version>
<name>JJWT</name>
<description>JSON Web Token support for the JVM and Android</description>
<packaging>pom</packaging>
@ -50,8 +50,8 @@
<scm>
<connection>scm:git:https://github.com/jwtk/jjwt.git</connection>
<developerConnection>scm:git:git@github.com:jwtk/jjwt.git</developerConnection>
<url>git@github.com:jwtk/jjwt.git</url>
<developerConnection>scm:git:https://github.com/jwtk/jjwt.git</developerConnection>
<url>https://github.com/jwtk/jjwt.git</url>
<tag>HEAD</tag>
</scm>
<issueManagement>
@ -325,6 +325,7 @@
<exclude>.gitattributes</exclude>
<exclude>**/genkeys</exclude>
<exclude>**/softhsm</exclude>
<exclude>**.adoc</exclude>
</excludes>
</licenseSet>
</licenseSets>

View File

@ -21,7 +21,7 @@
<parent>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-root</artifactId>
<version>0.12.4-SNAPSHOT</version>
<version>0.12.6-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>