mirror of https://github.com/jwtk/jjwt.git
Merge remote-tracking branch 'origin/master' into 60-convenience_expiration_setter_which_takes_a_duration
This commit is contained in:
commit
4a76b69d59
|
@ -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'
|
||||
|
|
98
CHANGELOG.md
98
CHANGELOG.md
|
@ -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
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
*
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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.");
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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. " +
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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'])
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
7
pom.xml
7
pom.xml
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
Loading…
Reference in New Issue