mirror of https://github.com/jwtk/jjwt.git
Audience claim string arrays (#819)
* Ensured `aud` claim is an array by default, but allows a single string value on creation for recipients that do not understand array values: - ClaimsMutator#audience(String) now appends to the `aud` set, and may be called multiple times - Added new ClaimsMutator#audience(Collection) method for setting/full replacement - Added new ClaimsMutator#audienceSingle for setting/full replacement of single string value - Marked ClaimsMutator#audienceSingle as Deprecated (even though it's not!) to discourage its use when possible.
This commit is contained in:
parent
524429ea3f
commit
ffbe9477da
|
@ -3322,11 +3322,11 @@ String jwe = Jwts.builder().audience("Alice")
|
|||
.compact();
|
||||
|
||||
// Alice receives and decrypts the compact JWE:
|
||||
String audience = Jwts.parser()
|
||||
Set<String> audience = Jwts.parser()
|
||||
.decryptWith(pair.getPrivate()) // <-- Alice's RSA private key
|
||||
.build().parseClaimsJwe(jwe).getPayload().getAudience();
|
||||
|
||||
assert "Alice".equals(audience);
|
||||
assert audience.contains("Alice");
|
||||
```
|
||||
|
||||
<a name="example-jwe-aeskw"></a>
|
||||
|
@ -3390,11 +3390,11 @@ String jwe = Jwts.builder().audience("Alice")
|
|||
.compact();
|
||||
|
||||
// Alice receives and decrypts the compact JWE:
|
||||
String audience = Jwts.parser()
|
||||
Set<String> audience = Jwts.parser()
|
||||
.decryptWith(pair.getPrivate()) // <-- Alice's EC private key
|
||||
.build().parseClaimsJwe(jwe).getPayload().getAudience();
|
||||
|
||||
assert "Alice".equals(audience);
|
||||
assert audience.contains("Alice");
|
||||
```
|
||||
|
||||
<a name="example-jwe-password"></a>
|
||||
|
|
|
@ -17,6 +17,7 @@ package io.jsonwebtoken;
|
|||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A JWT <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4">Claims set</a>.
|
||||
|
@ -38,25 +39,39 @@ import java.util.Map;
|
|||
*/
|
||||
public interface Claims extends Map<String, Object>, Identifiable {
|
||||
|
||||
/** JWT {@code Issuer} claims parameter name: <code>"iss"</code> */
|
||||
/**
|
||||
* JWT {@code Issuer} claims parameter name: <code>"iss"</code>
|
||||
*/
|
||||
String ISSUER = "iss";
|
||||
|
||||
/** JWT {@code Subject} claims parameter name: <code>"sub"</code> */
|
||||
/**
|
||||
* JWT {@code Subject} claims parameter name: <code>"sub"</code>
|
||||
*/
|
||||
String SUBJECT = "sub";
|
||||
|
||||
/** JWT {@code Audience} claims parameter name: <code>"aud"</code> */
|
||||
/**
|
||||
* JWT {@code Audience} claims parameter name: <code>"aud"</code>
|
||||
*/
|
||||
String AUDIENCE = "aud";
|
||||
|
||||
/** JWT {@code Expiration} claims parameter name: <code>"exp"</code> */
|
||||
/**
|
||||
* JWT {@code Expiration} claims parameter name: <code>"exp"</code>
|
||||
*/
|
||||
String EXPIRATION = "exp";
|
||||
|
||||
/** JWT {@code Not Before} claims parameter name: <code>"nbf"</code> */
|
||||
/**
|
||||
* JWT {@code Not Before} claims parameter name: <code>"nbf"</code>
|
||||
*/
|
||||
String NOT_BEFORE = "nbf";
|
||||
|
||||
/** JWT {@code Issued At} claims parameter name: <code>"iat"</code> */
|
||||
/**
|
||||
* JWT {@code Issued At} claims parameter name: <code>"iat"</code>
|
||||
*/
|
||||
String ISSUED_AT = "iat";
|
||||
|
||||
/** JWT {@code JWT ID} claims parameter name: <code>"jti"</code> */
|
||||
/**
|
||||
* JWT {@code JWT ID} claims parameter name: <code>"jti"</code>
|
||||
*/
|
||||
String ID = "jti";
|
||||
|
||||
/**
|
||||
|
@ -81,7 +96,7 @@ public interface Claims extends Map<String, Object>, Identifiable {
|
|||
*
|
||||
* @return the JWT {@code aud} value or {@code null} if not present.
|
||||
*/
|
||||
String getAudience();
|
||||
Set<String> getAudience();
|
||||
|
||||
/**
|
||||
* Returns the JWT <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.4">
|
||||
|
@ -124,7 +139,8 @@ public interface Claims extends Map<String, Object>, Identifiable {
|
|||
*
|
||||
* @return the JWT {@code jti} value or {@code null} if not present.
|
||||
*/
|
||||
@Override // just for JavaDoc specific to the JWT spec
|
||||
@Override
|
||||
// just for JavaDoc specific to the JWT spec
|
||||
String getId();
|
||||
|
||||
/**
|
||||
|
@ -133,14 +149,15 @@ public interface Claims extends Map<String, Object>, Identifiable {
|
|||
* <p>JJWT only converts simple String, Date, Long, Integer, Short and Byte types automatically. Anything more
|
||||
* complex is expected to be already converted to your desired type by the JSON
|
||||
* {@link io.jsonwebtoken.io.Deserializer Deserializer} implementation. You may specify a custom Deserializer for a
|
||||
* JwtParser with the desired conversion configuration via the {@link JwtParserBuilder#deserializeJsonWith} method.
|
||||
* JwtParser with the desired conversion configuration via the
|
||||
* {@link JwtParserBuilder#deserializer deserializer} method.
|
||||
* See <a href="https://github.com/jwtk/jjwt#custom-json-processor">custom JSON processor</a> for more
|
||||
* information. If using Jackson, you can specify custom claim POJO types as described in
|
||||
* <a href="https://github.com/jwtk/jjwt#json-jackson-custom-types">custom claim types</a>.
|
||||
*
|
||||
* @param claimName name of claim
|
||||
* @param claimName name of claim
|
||||
* @param requiredType the type of the value expected to be returned
|
||||
* @param <T> the type of the value expected to be returned
|
||||
* @param <T> the type of the value expected to be returned
|
||||
* @return the JWT {@code claimName} value or {@code null} if not present.
|
||||
* @throws RequiredTypeException throw if the claim value is not null and not of type {@code requiredType}
|
||||
*/
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package io.jsonwebtoken;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
|
@ -72,8 +73,12 @@ public interface ClaimsMutator<T extends ClaimsMutator<T>> {
|
|||
T subject(String sub);
|
||||
|
||||
/**
|
||||
* Sets the JWT <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.3">
|
||||
* <code>aud</code></a> (audience) value. A {@code null} value will remove the property from the JSON map.
|
||||
* Sets the JWT <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.3"><code>aud</code> (audience)
|
||||
* Claim</a> as <em>a single String, <b>NOT</b> a String array</em>. This method exists only for producing
|
||||
* JWTs sent to legacy recipients that are unable to interpret the {@code aud} value as a JSON String Array; it is
|
||||
* strongly recommended to avoid calling this method whenever possible and favor the
|
||||
* {@link #audience(String)} or {@link #audience(Collection)} methods instead, as they ensure a single deterministic
|
||||
* data type for recipients.
|
||||
*
|
||||
* @param aud the JWT {@code aud} value or {@code null} to remove the property from the JSON map.
|
||||
* @return the {@code Claims} instance for method chaining.
|
||||
|
@ -84,14 +89,45 @@ public interface ClaimsMutator<T extends ClaimsMutator<T>> {
|
|||
T setAudience(String aud);
|
||||
|
||||
/**
|
||||
* Sets the JWT <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.3">
|
||||
* <code>aud</code></a> (audience) value. A {@code null} value will remove the property from the JSON map.
|
||||
* Sets the JWT <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.3"><code>aud</code> (audience)
|
||||
* Claim</a> as <em>a single String, <b>NOT</b> a String array</em>. This method exists only for producing
|
||||
* JWTs sent to legacy recipients that are unable to interpret the {@code aud} value as a JSON String Array; it is
|
||||
* strongly recommended to avoid calling this method whenever possible and favor the
|
||||
* {@link #audience(String)} or {@link #audience(Collection)} methods instead, as they ensure a single deterministic
|
||||
* data type for recipients.
|
||||
*
|
||||
* @param aud the JWT {@code aud} value or {@code null} to remove the property from the JSON map.
|
||||
* @param aud the value to use as the {@code aud} Claim single-String value (and not an array of Strings), or
|
||||
* {@code null} to remove the property from the JSON map.
|
||||
* @return the instance for method chaining
|
||||
* @since JJWT_RELEASE_VERSION
|
||||
* @deprecated This is technically not deprecated because the JWT RFC mandates support for single string values,
|
||||
* but it is marked as deprecated to discourage its use when possible.
|
||||
*/
|
||||
// DO NOT REMOVE EVER. This is a required RFC feature, but marked as deprecated to discourage its use
|
||||
@Deprecated
|
||||
T audienceSingle(String aud);
|
||||
|
||||
/**
|
||||
* Adds the specified {@code aud} value to the {@link #audience(Collection) audience} Claim set (JSON Array). This
|
||||
* method may be called multiple times.
|
||||
*
|
||||
* @param aud a JWT {@code aud} value to add to the {@link #audience(Collection) audience} Claim set.
|
||||
* @return the {@code Claims} instance for method chaining.
|
||||
* @throws IllegalArgumentException if the {@code aud} argument is null or empty.
|
||||
* @since JJWT_RELEASE_VERSION
|
||||
*/
|
||||
T audience(String aud);
|
||||
T audience(String aud) throws IllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Sets the JWT <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.3"><code>aud</code></a> (audience)
|
||||
* Claim set, replacing any previous value(s).
|
||||
*
|
||||
* @param aud the values to set as the {@code aud} Claim set (JSON Array), or {@code null}/empty to remove the
|
||||
* {@code aud} claim from the JSON map entirely.
|
||||
* @return the instance for method chaining
|
||||
* @since JJWT_RELEASE_VERSION
|
||||
*/
|
||||
T audience(Collection<String> aud);
|
||||
|
||||
/**
|
||||
* Sets the JWT <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.4">
|
||||
|
|
|
@ -131,7 +131,7 @@ public interface JwtParserBuilder extends Builder<JwtParser> {
|
|||
|
||||
/**
|
||||
* Ensures that the specified {@code aud} exists in the parsed JWT. If missing or if the parsed
|
||||
* value does not equal the specified value, an exception will be thrown indicating that the
|
||||
* value does not contain the specified value, an exception will be thrown indicating that the
|
||||
* JWT is invalid and may not be used.
|
||||
*
|
||||
* @param audience the required value of the {@code aud} header parameter.
|
||||
|
@ -328,7 +328,7 @@ public interface JwtParserBuilder extends Builder<JwtParser> {
|
|||
* {@link #verifyWith(SecretKey)} for type safety, to reflect accurate naming of the concept, and for name
|
||||
* congruence with the {@link #decryptWith(SecretKey)} method.</p>
|
||||
*
|
||||
* <p>This method merely delegates directly to {@link #verifyWith(SecretKey) or {@link #verifyWith(PublicKey)}}.</p>
|
||||
* <p>This method merely delegates directly to {@link #verifyWith(SecretKey)} or {@link #verifyWith(PublicKey)}}.</p>
|
||||
*
|
||||
* @param key the algorithm-specific signature verification key to use to verify all encountered JWS digital
|
||||
* signatures.
|
||||
|
|
|
@ -83,6 +83,24 @@ public final class Collections {
|
|||
return java.util.Collections.unmodifiableList(Arrays.asList(elements));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the specified collection as a {@link Set} instance.
|
||||
*
|
||||
* @param c the collection to represent as a set
|
||||
* @param <T> collection element type
|
||||
* @return a type-safe immutable {@code Set} containing the specified collection elements.
|
||||
* @since JJWT_RELEASE_VERSION
|
||||
*/
|
||||
public static <T> Set<T> asSet(Collection<T> c) {
|
||||
if (c instanceof Set) {
|
||||
return (Set<T>) c;
|
||||
}
|
||||
if (isEmpty(c)) {
|
||||
return java.util.Collections.emptySet();
|
||||
}
|
||||
return java.util.Collections.unmodifiableSet(new LinkedHashSet<>(c));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a type-safe immutable {@code Set} containing the specified array elements.
|
||||
*
|
||||
|
|
|
@ -16,8 +16,23 @@
|
|||
package io.jsonwebtoken.security;
|
||||
|
||||
import java.security.PrivateKey;
|
||||
import java.security.Provider;
|
||||
import java.security.PublicKey;
|
||||
|
||||
/**
|
||||
* A builder that allows a {@code PrivateKey} to be transparently associated with a {@link #provider(Provider)} or
|
||||
* {@link #publicKey(PublicKey)} if necessary for algorithms that require them.
|
||||
*
|
||||
* @since JJWT_RELEASE_VERSION
|
||||
*/
|
||||
public interface PrivateKeyBuilder extends KeyBuilder<PrivateKey, PrivateKeyBuilder> {
|
||||
|
||||
/**
|
||||
* Sets the private key's corresponding {@code PublicKey} so that its public key material will be available to
|
||||
* algorithms that require it.
|
||||
*
|
||||
* @param publicKey the private key's corresponding {@code PublicKey}
|
||||
* @return the builder for method chaining.
|
||||
*/
|
||||
PrivateKeyBuilder publicKey(PublicKey publicKey);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright © 2023 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.lang
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import static org.junit.Assert.*
|
||||
|
||||
class CollectionsTest {
|
||||
|
||||
@Test
|
||||
void testAsSetFromNull() {
|
||||
assertSame java.util.Collections.emptySet(), Collections.asSet(null)
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAsSetFromEmpty() {
|
||||
def list = []
|
||||
assertSame java.util.Collections.emptySet(), Collections.asSet(list)
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAsSetFromSet() {
|
||||
def set = Collections.setOf('foo')
|
||||
assertSame set, Collections.asSet(set)
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAsSetFromList() {
|
||||
def list = Collections.of('one', 'two')
|
||||
def set = Collections.asSet(list)
|
||||
assertTrue set.containsAll(list)
|
||||
try {
|
||||
set.add('another')
|
||||
fail()
|
||||
} catch (UnsupportedOperationException ignored) { // expected, asSet returns immutable instances
|
||||
}
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@ import io.jsonwebtoken.lang.Registry;
|
|||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class DefaultClaims extends FieldMap implements Claims {
|
||||
|
||||
|
@ -38,7 +39,7 @@ public class DefaultClaims extends FieldMap implements Claims {
|
|||
|
||||
static final Field<String> ISSUER = Fields.string(Claims.ISSUER, "Issuer");
|
||||
static final Field<String> SUBJECT = Fields.string(Claims.SUBJECT, "Subject");
|
||||
static final Field<String> AUDIENCE = Fields.string(Claims.AUDIENCE, "Audience");
|
||||
static final Field<Set<String>> AUDIENCE = Fields.stringSet(Claims.AUDIENCE, "Audience");
|
||||
static final Field<Date> EXPIRATION = Fields.rfcDate(Claims.EXPIRATION, "Expiration Time");
|
||||
static final Field<Date> NOT_BEFORE = Fields.rfcDate(Claims.NOT_BEFORE, "Not Before");
|
||||
static final Field<Date> ISSUED_AT = Fields.rfcDate(Claims.ISSUED_AT, "Issued At");
|
||||
|
@ -51,6 +52,10 @@ public class DefaultClaims extends FieldMap implements Claims {
|
|||
super(FIELDS);
|
||||
}
|
||||
|
||||
public DefaultClaims(FieldMap m) {
|
||||
super(m.FIELDS, m);
|
||||
}
|
||||
|
||||
public DefaultClaims(Map<String, ?> map) {
|
||||
super(FIELDS, map);
|
||||
}
|
||||
|
@ -71,7 +76,7 @@ public class DefaultClaims extends FieldMap implements Claims {
|
|||
}
|
||||
|
||||
@Override
|
||||
public String getAudience() {
|
||||
public Set<String> getAudience() {
|
||||
return get(AUDIENCE);
|
||||
}
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ public class DefaultJweHeaderMutator<T extends JweHeaderMutator<T>>
|
|||
// MapMutator methods
|
||||
// =============================================================
|
||||
|
||||
private T put(Field<?> field, Object value) {
|
||||
private <F> T put(Field<F> field, F value) {
|
||||
this.DELEGATE.put(field, value);
|
||||
return self();
|
||||
}
|
||||
|
|
|
@ -61,6 +61,7 @@ import java.security.PrivateKey;
|
|||
import java.security.Provider;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
|
@ -393,7 +394,14 @@ public class DefaultJwtBuilder implements JwtBuilder {
|
|||
|
||||
@Override
|
||||
public JwtBuilder setAudience(String aud) {
|
||||
return audience(aud);
|
||||
this.claimsBuilder.setAudience(aud);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JwtBuilder audienceSingle(String aud) {
|
||||
this.claimsBuilder.audienceSingle(aud);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -402,6 +410,12 @@ public class DefaultJwtBuilder implements JwtBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JwtBuilder audience(Collection<String> aud) {
|
||||
this.claimsBuilder.audience(aud);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JwtBuilder setExpiration(Date exp) {
|
||||
return expiration(exp);
|
||||
|
|
|
@ -52,6 +52,7 @@ import io.jsonwebtoken.io.DeserializationException;
|
|||
import io.jsonwebtoken.io.Deserializer;
|
||||
import io.jsonwebtoken.lang.Arrays;
|
||||
import io.jsonwebtoken.lang.Assert;
|
||||
import io.jsonwebtoken.lang.Collections;
|
||||
import io.jsonwebtoken.lang.DateFormats;
|
||||
import io.jsonwebtoken.lang.Strings;
|
||||
import io.jsonwebtoken.security.AeadAlgorithm;
|
||||
|
@ -89,6 +90,9 @@ public class DefaultJwtParser implements JwtParser {
|
|||
"used to encrypt, and PrivateKeys are used to decrypt.";
|
||||
|
||||
public static final String INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE = "Expected %s claim to be: %s, but was: %s.";
|
||||
|
||||
public static final String MISSING_EXPECTED_CLAIM_VALUE_MESSAGE_TEMPLATE =
|
||||
"Missing expected '%s' value in '%s' claim %s.";
|
||||
public static final String MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE = "Expected %s claim to be: %s, but was not " +
|
||||
"present in the JWT claims.";
|
||||
|
||||
|
@ -627,9 +631,25 @@ public class DefaultJwtParser implements JwtParser {
|
|||
}
|
||||
|
||||
if (actualClaimValue == null) {
|
||||
String msg = String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE,
|
||||
expectedClaimName, expectedClaimValue);
|
||||
boolean collection = expectedClaimValue instanceof Collection;
|
||||
String msg = "Missing '" + expectedClaimName + "' claim. Expected value";
|
||||
if (collection) {
|
||||
msg += "s: " + expectedClaimValue;
|
||||
} else {
|
||||
msg += ": " + expectedClaimValue;
|
||||
}
|
||||
throw new MissingClaimException(header, claims, expectedClaimName, expectedClaimValue, msg);
|
||||
} else if (expectedClaimValue instanceof Collection) {
|
||||
Collection<?> expectedValues = (Collection<?>) expectedClaimValue;
|
||||
Collection<?> actualValues = actualClaimValue instanceof Collection ? (Collection<?>) actualClaimValue :
|
||||
Collections.setOf(actualClaimValue);
|
||||
for (Object expectedValue : expectedValues) {
|
||||
if (!Collections.contains(actualValues.iterator(), expectedValue)) {
|
||||
String msg = String.format(MISSING_EXPECTED_CLAIM_VALUE_MESSAGE_TEMPLATE,
|
||||
expectedValue, expectedClaimName, actualValues);
|
||||
throw new IncorrectClaimException(header, claims, expectedClaimName, expectedClaimValue, msg);
|
||||
}
|
||||
}
|
||||
} else if (!expectedClaimValue.equals(actualClaimValue)) {
|
||||
String msg = String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE,
|
||||
expectedClaimName, expectedClaimValue, actualClaimValue);
|
||||
|
|
|
@ -155,7 +155,7 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder {
|
|||
|
||||
@Override
|
||||
public JwtParserBuilder requireAudience(String audience) {
|
||||
expectedClaims.setAudience(audience);
|
||||
expectedClaims.audience(audience);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
|
@ -18,9 +18,16 @@ package io.jsonwebtoken.impl;
|
|||
import io.jsonwebtoken.ClaimsMutator;
|
||||
import io.jsonwebtoken.impl.lang.DelegatingMapMutator;
|
||||
import io.jsonwebtoken.impl.lang.Field;
|
||||
import io.jsonwebtoken.impl.lang.Fields;
|
||||
import io.jsonwebtoken.lang.Assert;
|
||||
import io.jsonwebtoken.lang.Collections;
|
||||
import io.jsonwebtoken.lang.MapMutator;
|
||||
import io.jsonwebtoken.lang.Strings;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @param <T> subclass type
|
||||
|
@ -30,15 +37,22 @@ public class DelegatingClaimsMutator<T extends MapMutator<String, Object, T> & C
|
|||
extends DelegatingMapMutator<String, Object, FieldMap, T>
|
||||
implements ClaimsMutator<T> {
|
||||
|
||||
private static final Field<String> AUDIENCE_STRING =
|
||||
Fields.string(DefaultClaims.AUDIENCE.getId(), DefaultClaims.AUDIENCE.getName());
|
||||
|
||||
protected DelegatingClaimsMutator() {
|
||||
super(new FieldMap(DefaultClaims.FIELDS));
|
||||
}
|
||||
|
||||
<F> T put(Field<F> field, Object value) {
|
||||
<F> T put(Field<F> field, F value) {
|
||||
this.DELEGATE.put(field, value);
|
||||
return self();
|
||||
}
|
||||
|
||||
<F> F get(Field<F> field) {
|
||||
return this.DELEGATE.get(field);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T setIssuer(String iss) {
|
||||
return issuer(iss);
|
||||
|
@ -61,12 +75,49 @@ public class DelegatingClaimsMutator<T extends MapMutator<String, Object, T> & C
|
|||
|
||||
@Override
|
||||
public T setAudience(String aud) {
|
||||
return audience(aud);
|
||||
return audienceSingle(aud);
|
||||
}
|
||||
|
||||
private Set<String> getAudience() {
|
||||
// caller expects that we're working with a String<Set> so ensure that:
|
||||
if (!this.DELEGATE.FIELDS.get(AUDIENCE_STRING.getId()).supports(Collections.emptySet())) {
|
||||
String existing = get(AUDIENCE_STRING);
|
||||
remove(AUDIENCE_STRING.getId()); // clear out any canonical/idiomatic values since we're replacing
|
||||
setDelegate(this.DELEGATE.replace(DefaultClaims.AUDIENCE));
|
||||
if (Strings.hasText(existing)) {
|
||||
put(DefaultClaims.AUDIENCE, Collections.setOf(existing)); // replace as Set
|
||||
}
|
||||
}
|
||||
Set<String> aud = get(DefaultClaims.AUDIENCE);
|
||||
return aud != null ? aud : Collections.<String>emptySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public T audienceSingle(String aud) {
|
||||
if (!Strings.hasText(aud)) {
|
||||
return put(DefaultClaims.AUDIENCE, null);
|
||||
}
|
||||
// otherwise it's an actual single string, we need to ensure that we can represent it as a single
|
||||
// string by swapping out the AUDIENCE field if necessary:
|
||||
if (this.DELEGATE.FIELDS.get(AUDIENCE_STRING.getId()).supports(Collections.emptySet())) { // need to swap:
|
||||
remove(AUDIENCE_STRING.getId()); //remove any existing value, as conversion will throw an exception
|
||||
setDelegate(this.DELEGATE.replace(AUDIENCE_STRING));
|
||||
}
|
||||
return put(AUDIENCE_STRING, aud);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T audience(String aud) {
|
||||
return put(DefaultClaims.AUDIENCE, aud);
|
||||
aud = Assert.hasText(Strings.clean(aud), "Audience string value cannot be null or empty.");
|
||||
Set<String> set = new LinkedHashSet<>(getAudience());
|
||||
set.add(aud);
|
||||
return audience(set);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T audience(Collection<String> aud) {
|
||||
getAudience(); //coerce to Set<String> if necessary
|
||||
return put(DefaultClaims.AUDIENCE, Collections.asSet(aud));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -82,6 +82,11 @@ public class FieldMap implements Map<String, Object>, FieldReadable, Nameable {
|
|||
}
|
||||
}
|
||||
|
||||
protected FieldMap replace(Field<?> field) {
|
||||
Registry<String, ? extends Field<?>> registry = Fields.replace(this.FIELDS, field);
|
||||
return new FieldMap(registry, this, this.mutable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Map";
|
||||
|
@ -128,37 +133,39 @@ public class FieldMap implements Map<String, Object>, FieldReadable, Nameable {
|
|||
return values.get(o);
|
||||
}
|
||||
|
||||
private static Object clean(Object o) {
|
||||
if (o instanceof String) {
|
||||
o = Strings.clean((String) o);
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to put a value for a canonical field.
|
||||
* Convenience method to put a value for an idiomatic field.
|
||||
*
|
||||
* @param field the field representing the property name to set
|
||||
* @param value the value to set
|
||||
* @return the previous value for the field name, or {@code null} if there was no previous value
|
||||
* @return the previous value for the field, or {@code null} if there was no previous value
|
||||
* @since JJWT_RELEASE_VERSION
|
||||
*/
|
||||
protected Object put(Field<?> field, Object value) {
|
||||
return put(field.getId(), value);
|
||||
protected final <T> Object put(Field<T> field, Object value) {
|
||||
assertMutable();
|
||||
Assert.notNull(field, "Field cannot be null.");
|
||||
Assert.hasText(field.getId(), "Field id cannot be null or empty.");
|
||||
return apply(field, clean(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object put(String name, Object value) {
|
||||
public final Object put(String name, Object value) {
|
||||
assertMutable();
|
||||
name = Assert.notNull(Strings.clean(name), "Member name cannot be null or empty.");
|
||||
if (value instanceof String) {
|
||||
value = Strings.clean((String) value);
|
||||
}
|
||||
return idiomaticPut(name, value);
|
||||
}
|
||||
|
||||
// ensures that if a property name matches an RFC-specified name, that value can be represented
|
||||
// as an idiomatic type-safe Java value in addition to the canonical RFC/encoded value.
|
||||
private Object idiomaticPut(String name, Object value) {
|
||||
Assert.stateNotNull(name, "Name cannot be null."); // asserted by caller
|
||||
Field<?> field = FIELDS.get(name);
|
||||
if (field != null) { //Setting a JWA-standard property - let's ensure we can represent it idiomatically:
|
||||
return apply(field, value);
|
||||
} else { //non-standard/custom property:
|
||||
return nullSafePut(name, value);
|
||||
if (field != null) {
|
||||
// standard property, represent it idiomatically:
|
||||
return put(field, value);
|
||||
} else {
|
||||
// non-standard or custom property, just apply directly:
|
||||
return nullSafePut(name, clean(value));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
package io.jsonwebtoken.impl.lang;
|
||||
|
||||
import io.jsonwebtoken.lang.Arrays;
|
||||
import io.jsonwebtoken.lang.Assert;
|
||||
import io.jsonwebtoken.lang.Registry;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
@ -23,8 +24,10 @@ import java.net.URI;
|
|||
import java.security.cert.X509Certificate;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public final class Fields {
|
||||
|
@ -84,4 +87,14 @@ public final class Fields {
|
|||
set.addAll(Arrays.asList(fields));
|
||||
return new IdRegistry<>("Field", set, true);
|
||||
}
|
||||
|
||||
public static Registry<String, ? extends Field<?>> replace(Registry<String, ? extends Field<?>> registry, Field<?> field) {
|
||||
Assert.notEmpty(registry, "Registry cannot be null or empty.");
|
||||
Assert.notNull(field, "Field cannot be null.");
|
||||
String id = Assert.hasText(field.getId(), "Field id cannot be null or empty.");
|
||||
Map<String, Field<?>> newFields = new LinkedHashMap<>(registry);
|
||||
newFields.remove(id); // remove old/default
|
||||
newFields.put(id, field); // add new one
|
||||
return registry(newFields.values());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import io.jsonwebtoken.impl.lang.Field;
|
|||
import io.jsonwebtoken.impl.lang.Fields;
|
||||
import io.jsonwebtoken.lang.Assert;
|
||||
import io.jsonwebtoken.lang.Collections;
|
||||
import io.jsonwebtoken.lang.Registry;
|
||||
import io.jsonwebtoken.security.HashAlgorithm;
|
||||
import io.jsonwebtoken.security.Jwks;
|
||||
import io.jsonwebtoken.security.KeyOperation;
|
||||
|
@ -30,7 +31,6 @@ import java.security.Provider;
|
|||
import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
@ -124,14 +124,11 @@ public class DefaultJwkContext<K extends Key> extends AbstractX509Context<JwkCon
|
|||
|
||||
@Override
|
||||
public JwkContext<K> field(Field<?> field) {
|
||||
Assert.notNull(field, "Field cannot be null.");
|
||||
Map<String, Field<?>> newFields = new LinkedHashMap<>(this.FIELDS);
|
||||
newFields.remove(field.getId()); // remove old/default
|
||||
newFields.put(field.getId(), field); // add new one
|
||||
Set<Field<?>> fieldSet = new LinkedHashSet<>(newFields.values());
|
||||
Registry<String, ? extends Field<?>> registry = Fields.replace(this.FIELDS, field);
|
||||
Set<Field<?>> fields = new LinkedHashSet<>(registry.values());
|
||||
return this.key != null ?
|
||||
new DefaultJwkContext<>(fieldSet, this, key) :
|
||||
new DefaultJwkContext<K>(fieldSet, this, false);
|
||||
new DefaultJwkContext<>(fields, this, key) :
|
||||
new DefaultJwkContext<K>(fields, this, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -31,7 +31,7 @@ import java.security.SecureRandom
|
|||
|
||||
import static io.jsonwebtoken.DateTestUtils.truncateMillis
|
||||
import static io.jsonwebtoken.impl.DefaultJwtParser.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE
|
||||
import static io.jsonwebtoken.impl.DefaultJwtParser.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE
|
||||
import static io.jsonwebtoken.impl.DefaultJwtParser.MISSING_EXPECTED_CLAIM_VALUE_MESSAGE_TEMPLATE
|
||||
import static org.junit.Assert.*
|
||||
|
||||
@SuppressWarnings('GrDeprecatedAPIUsage')
|
||||
|
@ -958,10 +958,8 @@ class JwtParserTest {
|
|||
parseClaimsJws(compact)
|
||||
fail()
|
||||
} catch (MissingClaimException e) {
|
||||
assertEquals(
|
||||
String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, claimName, claimValue),
|
||||
e.getMessage()
|
||||
)
|
||||
String msg = "Missing '$claimName' claim. Expected value: $claimValue"
|
||||
assertEquals msg, e.getMessage()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1077,10 +1075,8 @@ class JwtParserTest {
|
|||
parseClaimsJws(compact)
|
||||
fail()
|
||||
} catch (MissingClaimException e) {
|
||||
assertEquals(
|
||||
String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.ISSUER, issuer),
|
||||
e.getMessage()
|
||||
)
|
||||
String msg = "Missing 'iss' claim. Expected value: $issuer"
|
||||
assertEquals msg, e.message
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1099,7 +1095,60 @@ class JwtParserTest {
|
|||
build().
|
||||
parseClaimsJws(compact)
|
||||
|
||||
assertEquals jwt.getPayload().getAudience(), audience
|
||||
assertEquals audience, jwt.getPayload().getAudience().iterator().next()
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseExpectedEqualAudiences() {
|
||||
def one = 'one'
|
||||
def two = 'two'
|
||||
def expected = [one, two]
|
||||
String jwt = Jwts.builder().audience(one).audience(two).compact()
|
||||
def aud = Jwts.parser().enableUnsecured().requireAudience(one).requireAudience(two).build()
|
||||
.parseClaimsJwt(jwt).getPayload().getAudience()
|
||||
assertEquals expected.size(), aud.size()
|
||||
assertTrue aud.containsAll(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseAtLeastOneAudiences() {
|
||||
def one = 'one'
|
||||
|
||||
String jwt = Jwts.builder().audience(one).audience('two').compact() // more audiences than required
|
||||
|
||||
def aud = Jwts.parser().enableUnsecured().requireAudience(one) // require only one
|
||||
.build().parseClaimsJwt(jwt).getPayload().getAudience()
|
||||
|
||||
assertNotNull aud
|
||||
assertTrue aud.contains(one)
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseMissingAudiences() {
|
||||
def one = 'one'
|
||||
def two = 'two'
|
||||
String jwt = Jwts.builder().id('foo').compact()
|
||||
try {
|
||||
Jwts.parser().enableUnsecured().requireAudience(one).requireAudience(two).build().parseClaimsJwt(jwt)
|
||||
fail()
|
||||
} catch (MissingClaimException expected) {
|
||||
String msg = "Missing 'aud' claim. Expected values: [$one, $two]"
|
||||
assertEquals msg, expected.message
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseSingleValueClaimExpectingMultipleValues() {
|
||||
def one = 'one'
|
||||
def two = 'two'
|
||||
def expected = [one, two]
|
||||
String jwt = Jwts.builder().claim('custom', one).compact()
|
||||
try {
|
||||
Jwts.parser().enableUnsecured().require('custom', expected).build().parseClaimsJwt(jwt)
|
||||
} catch (IncorrectClaimException e) {
|
||||
String msg = "Missing expected '$two' value in 'custom' claim [$one]."
|
||||
assertEquals msg, e.message
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -1120,10 +1169,9 @@ class JwtParserTest {
|
|||
parseClaimsJws(compact)
|
||||
fail()
|
||||
} catch (IncorrectClaimException e) {
|
||||
assertEquals(
|
||||
String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.AUDIENCE, goodAudience, badAudience),
|
||||
e.getMessage()
|
||||
)
|
||||
String msg = String.format(MISSING_EXPECTED_CLAIM_VALUE_MESSAGE_TEMPLATE, goodAudience,
|
||||
Claims.AUDIENCE, [badAudience])
|
||||
assertEquals msg, e.getMessage()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1144,10 +1192,8 @@ class JwtParserTest {
|
|||
parseClaimsJws(compact)
|
||||
fail()
|
||||
} catch (MissingClaimException e) {
|
||||
assertEquals(
|
||||
String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.AUDIENCE, audience),
|
||||
e.getMessage()
|
||||
)
|
||||
String msg = "Missing 'aud' claim. Expected values: [$audience]"
|
||||
assertEquals msg, e.message
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1211,10 +1257,8 @@ class JwtParserTest {
|
|||
parseClaimsJws(compact)
|
||||
fail()
|
||||
} catch (MissingClaimException e) {
|
||||
assertEquals(
|
||||
String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.SUBJECT, subject),
|
||||
e.getMessage()
|
||||
)
|
||||
String msg = "Missing 'sub' claim. Expected value: $subject"
|
||||
assertEquals msg, e.getMessage()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1278,10 +1322,8 @@ class JwtParserTest {
|
|||
parseClaimsJws(compact)
|
||||
fail()
|
||||
} catch (MissingClaimException e) {
|
||||
assertEquals(
|
||||
String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.ID, id),
|
||||
e.getMessage()
|
||||
)
|
||||
String msg = "Missing 'jti' claim. Expected value: $id"
|
||||
assertEquals msg, e.getMessage()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1477,10 +1519,8 @@ class JwtParserTest {
|
|||
parseClaimsJws(compact)
|
||||
fail()
|
||||
} catch (MissingClaimException e) {
|
||||
assertEquals(
|
||||
String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, "aDate", aDate),
|
||||
e.getMessage()
|
||||
)
|
||||
String msg = "Missing 'aDate' claim. Expected value: $aDate"
|
||||
assertEquals msg, e.getMessage()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -351,7 +351,7 @@ class JwtsTest {
|
|||
void testConvenienceAudience() {
|
||||
String compact = Jwts.builder().setAudience("You").compact()
|
||||
Claims claims = Jwts.parser().enableUnsecured().build().parse(compact).payload as Claims
|
||||
assertEquals 'You', claims.getAudience()
|
||||
assertEquals 'You', claims.getAudience().iterator().next()
|
||||
|
||||
compact = Jwts.builder().setIssuer("Me")
|
||||
.setAudience("You") //set it
|
||||
|
@ -437,17 +437,17 @@ class JwtsTest {
|
|||
|
||||
String id = UUID.randomUUID().toString()
|
||||
|
||||
String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(key, alg)
|
||||
String compact = Jwts.builder().id(id).issuer("an issuer").signWith(key, alg)
|
||||
.claim("state", "hello this is an amazing jwt").compact()
|
||||
|
||||
def jws = Jwts.parser().setSigningKey(key).build().parseClaimsJws(compact)
|
||||
def jws = Jwts.parser().verifyWith(key).build().parseClaimsJws(compact)
|
||||
|
||||
Claims claims = jws.payload
|
||||
|
||||
assertNull jws.header.getCompressionAlgorithm()
|
||||
|
||||
assertEquals id, claims.getId()
|
||||
assertEquals "an audience", claims.getAudience()
|
||||
assertEquals "an issuer", claims.getIssuer()
|
||||
assertEquals "hello this is an amazing jwt", claims.state
|
||||
}
|
||||
|
||||
|
@ -459,17 +459,17 @@ class JwtsTest {
|
|||
|
||||
String id = UUID.randomUUID().toString()
|
||||
|
||||
String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(key, alg)
|
||||
String compact = Jwts.builder().id(id).issuer("an issuer").signWith(key, alg)
|
||||
.claim("state", "hello this is an amazing jwt").compressWith(Jwts.ZIP.DEF).compact()
|
||||
|
||||
def jws = Jwts.parser().setSigningKey(key).build().parseClaimsJws(compact)
|
||||
def jws = Jwts.parser().verifyWith(key).build().parseClaimsJws(compact)
|
||||
|
||||
Claims claims = jws.payload
|
||||
|
||||
assertEquals "DEF", jws.header.getCompressionAlgorithm()
|
||||
|
||||
assertEquals id, claims.getId()
|
||||
assertEquals "an audience", claims.getAudience()
|
||||
assertEquals "an issuer", claims.getIssuer()
|
||||
assertEquals "hello this is an amazing jwt", claims.state
|
||||
}
|
||||
|
||||
|
@ -481,17 +481,17 @@ class JwtsTest {
|
|||
|
||||
String id = UUID.randomUUID().toString()
|
||||
|
||||
String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(key, alg)
|
||||
String compact = Jwts.builder().id(id).issuer("an issuer").signWith(key, alg)
|
||||
.claim("state", "hello this is an amazing jwt").compressWith(Jwts.ZIP.GZIP).compact()
|
||||
|
||||
def jws = Jwts.parser().setSigningKey(key).build().parseClaimsJws(compact)
|
||||
def jws = Jwts.parser().verifyWith(key).build().parseClaimsJws(compact)
|
||||
|
||||
Claims claims = jws.payload
|
||||
|
||||
assertEquals "GZIP", jws.header.getCompressionAlgorithm()
|
||||
|
||||
assertEquals id, claims.getId()
|
||||
assertEquals "an audience", claims.getAudience()
|
||||
assertEquals "an issuer", claims.getIssuer()
|
||||
assertEquals "hello this is an amazing jwt", claims.state
|
||||
}
|
||||
|
||||
|
@ -503,7 +503,7 @@ class JwtsTest {
|
|||
|
||||
String id = UUID.randomUUID().toString()
|
||||
|
||||
String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(key, alg)
|
||||
String compact = Jwts.builder().id(id).issuer("an issuer").signWith(key, alg)
|
||||
.claim("state", "hello this is an amazing jwt").compressWith(new GzipCompressionAlgorithm() {
|
||||
@Override
|
||||
String getId() {
|
||||
|
@ -511,7 +511,7 @@ class JwtsTest {
|
|||
}
|
||||
}).compact()
|
||||
|
||||
def jws = Jwts.parser().setSigningKey(key).setCompressionCodecResolver(new CompressionCodecResolver() {
|
||||
def jws = Jwts.parser().verifyWith(key).setCompressionCodecResolver(new CompressionCodecResolver() {
|
||||
@Override
|
||||
CompressionCodec resolveCompressionCodec(Header header) throws CompressionException {
|
||||
String algorithm = header.getCompressionAlgorithm()
|
||||
|
@ -529,7 +529,7 @@ class JwtsTest {
|
|||
assertEquals "CUSTOM", jws.header.getCompressionAlgorithm()
|
||||
|
||||
assertEquals id, claims.getId()
|
||||
assertEquals "an audience", claims.getAudience()
|
||||
assertEquals "an issuer", claims.getIssuer()
|
||||
assertEquals "hello this is an amazing jwt", claims.state
|
||||
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import io.jsonwebtoken.impl.security.Randoms
|
|||
import io.jsonwebtoken.impl.security.TestKey
|
||||
import io.jsonwebtoken.impl.security.TestKeys
|
||||
import io.jsonwebtoken.io.*
|
||||
import io.jsonwebtoken.jackson.io.JacksonDeserializer
|
||||
import io.jsonwebtoken.security.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
@ -559,4 +560,171 @@ class DefaultJwtBuilderTest {
|
|||
assertEquals msg, expected.getMessage()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAudienceSingle() {
|
||||
def key = TestKeys.HS256
|
||||
String audienceSingleString = 'test'
|
||||
def jwt = builder.audienceSingle(audienceSingleString).compact()
|
||||
// can't use the parser here to validate because it coerces the string value into an array automatically,
|
||||
// so we need to check the raw payload:
|
||||
def encoded = new JwtTokenizer().tokenize(jwt).getPayload()
|
||||
byte[] bytes = Decoders.BASE64URL.decode(encoded)
|
||||
Map<String, ?> claims = new JacksonDeserializer<>().deserialize(bytes) as Map<String, ?>
|
||||
|
||||
assertEquals audienceSingleString, claims.aud
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that an additional call to audienceSingle is a full replacement operation and fully replaces the
|
||||
* previous audienceSingle value
|
||||
*/
|
||||
@Test
|
||||
void testAudienceSingleMultiple() {
|
||||
def first = 'first'
|
||||
def second = 'second'
|
||||
def jwt = builder.audienceSingle(first).audienceSingle(second).compact()
|
||||
// can't use the parser here to validate because it coerces the string value into an array automatically,
|
||||
// so we need to check the raw payload:
|
||||
def encoded = new JwtTokenizer().tokenize(jwt).getPayload()
|
||||
byte[] bytes = Decoders.BASE64URL.decode(encoded)
|
||||
Map<String, ?> claims = new JacksonDeserializer<>().deserialize(bytes) as Map<String, ?>
|
||||
|
||||
assertEquals second, claims.aud // second audienceSingle call replaces first value
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that an additional call to audienceSingle is a full replacement operation and fully replaces the
|
||||
* previous audienceSingle value
|
||||
*/
|
||||
@Test
|
||||
void testAudienceSingleThenNull() {
|
||||
def jwt = builder.id('test')
|
||||
.audienceSingle('single') // set one
|
||||
.audienceSingle(null) // remove it entirely
|
||||
.compact()
|
||||
|
||||
// shouldn't be an audience at all:
|
||||
assertNull Jwts.parser().enableUnsecured().build().parseClaimsJwt(jwt).payload.getAudience()
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that, even if audienceSingle is called and then the value removed, a final call to audience(Collection)
|
||||
* still represents a collection without any value errors
|
||||
*/
|
||||
@Test
|
||||
void testAudienceSingleThenNullThenCollection() {
|
||||
def first = 'first'
|
||||
def second = 'second'
|
||||
def expected = [first, second] as Set<String>
|
||||
def jwt = builder.audienceSingle(first) // sets single value
|
||||
.audienceSingle(null) // removes entirely
|
||||
.audience([first, second]) // sets collection
|
||||
.compact()
|
||||
|
||||
def aud = Jwts.parser().enableUnsecured().build().parseClaimsJwt(jwt).payload.getAudience()
|
||||
assertEquals expected, aud
|
||||
}
|
||||
|
||||
/**
|
||||
* Test to ensure that if we receive a JWT with a single string value, that the parser coerces it to a String array
|
||||
* so we don't have to worry about different data types:
|
||||
*/
|
||||
@Test
|
||||
void testParseAudienceSingle() {
|
||||
def key = TestKeys.HS256
|
||||
String audienceSingleString = 'test'
|
||||
def jwt = builder.audienceSingle(audienceSingleString).compact()
|
||||
|
||||
assertEquals audienceSingleString, Jwts.parser().enableUnsecured().build().parseClaimsJwt(jwt).payload
|
||||
.getAudience().iterator().next() // a collection, not a single string
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAudience() {
|
||||
def aud = 'fubar'
|
||||
def jwt = Jwts.builder().audience(aud).compact()
|
||||
assertEquals aud, Jwts.parser().enableUnsecured().build().parseClaimsJwt(jwt).payload.getAudience().iterator().next()
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAudienceMultipleTimes() {
|
||||
def one = 'one'
|
||||
def two = 'two'
|
||||
def jwt = Jwts.builder().audience(one).audience(two).compact()
|
||||
def aud = Jwts.parser().enableUnsecured().build().parseClaimsJwt(jwt).payload.getAudience()
|
||||
assertTrue aud.contains(one)
|
||||
assertTrue aud.contains(two)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that if someone calls builder.audienceSingle and then audience(String), that the audience value
|
||||
* will automatically be coerced from a String to a Set<String> and contain both elements.
|
||||
*/
|
||||
@Test
|
||||
void testAudienceSingleThenAudience() {
|
||||
def one = 'one'
|
||||
def two = 'two'
|
||||
def jwt = Jwts.builder().audienceSingle(one).audience(two).compact()
|
||||
def aud = Jwts.parser().enableUnsecured().build().parseClaimsJwt(jwt).payload.getAudience()
|
||||
assertTrue aud.contains(one)
|
||||
assertTrue aud.contains(two)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that if someone calls builder.audience and then audienceSingle, that the audience value
|
||||
* will automatically be coerced to a single String contain only the single value since audienceSingle is a
|
||||
* full-replacement operation.
|
||||
*/
|
||||
@Test
|
||||
void testAudienceThenAudienceSingle() {
|
||||
def one = 'one'
|
||||
def two = 'two'
|
||||
def jwt = Jwts.builder().audience(one).audienceSingle(two).compact()
|
||||
|
||||
// can't use the parser here to validate because it coerces the string value into an array automatically,
|
||||
// so we need to check the raw payload:
|
||||
def encoded = new JwtTokenizer().tokenize(jwt).getPayload()
|
||||
byte[] bytes = Decoders.BASE64URL.decode(encoded)
|
||||
Map<String, ?> claims = new JacksonDeserializer<>().deserialize(bytes) as Map<String, ?>
|
||||
|
||||
assertEquals two, claims.aud
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that if someone calls builder.audienceSingle and then audience(Collection), the builder coerces the
|
||||
* aud to a Set<String> and only the elements in the Collection will be applied since audience(Collection) is a
|
||||
* full-replacement operation.
|
||||
*/
|
||||
@Test
|
||||
void testAudienceSingleThenAudienceCollection() {
|
||||
def single = 'one'
|
||||
def collection = ['two', 'three'] as Set<String>
|
||||
def jwt = Jwts.builder().audienceSingle(single).audience(collection).compact()
|
||||
def aud = Jwts.parser().enableUnsecured().build().parseClaimsJwt(jwt).payload.getAudience()
|
||||
assertEquals collection.size(), aud.size()
|
||||
assertTrue aud.containsAll(collection)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that if someone calls builder.audience(Collection) and then audienceSingle, that the audience value
|
||||
* will automatically be coerced to a single String contain only the single value since audienceSingle is a
|
||||
* full-replacement operation.
|
||||
*/
|
||||
@Test
|
||||
void testAudienceCollectionThenAudienceSingle() {
|
||||
def one = 'one'
|
||||
def two = 'two'
|
||||
def three = 'three'
|
||||
def jwt = Jwts.builder().audience([one, two]).audienceSingle(three).compact()
|
||||
|
||||
// can't use the parser here to validate because it coerces the string value into an array automatically,
|
||||
// so we need to check the raw payload:
|
||||
def encoded = new JwtTokenizer().tokenize(jwt).getPayload()
|
||||
byte[] bytes = Decoders.BASE64URL.decode(encoded)
|
||||
Map<String, ?> claims = new JacksonDeserializer<>().deserialize(bytes) as Map<String, ?>
|
||||
|
||||
assertEquals three, claims.aud
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ class DefaultJwtTest {
|
|||
void testToString() {
|
||||
String compact = Jwts.builder().header().add('foo', 'bar').and().audience('jsmith').compact()
|
||||
Jwt jwt = Jwts.parser().enableUnsecured().build().parseClaimsJwt(compact)
|
||||
assertEquals 'header={foo=bar, alg=none},payload={aud=jsmith}', jwt.toString()
|
||||
assertEquals 'header={foo=bar, alg=none},payload={aud=[jsmith]}', jwt.toString()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -235,14 +235,13 @@ class AbstractJwkBuilderTest {
|
|||
assertEquals canonical, jwk.key_ops
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testCustomOperationOverridesDefault() {
|
||||
def op = Jwks.OP.builder().id('sign').description('Different Description')
|
||||
.related(Jwks.OP.VERIFY.id).build()
|
||||
def builder = builder().operationPolicy(Jwks.OP.policy().add(op).build())
|
||||
def jwk = builder.operations(Collections.setOf(op, Jwks.OP.VERIFY)).build()
|
||||
println jwk
|
||||
def jwk = builder.operations(Collections.setOf(op, Jwks.OP.VERIFY)).build() as Jwk
|
||||
assertSame op, jwk.getOperations().find({it.id == 'sign'})
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -128,6 +128,6 @@ class EdSignatureAlgorithmTest {
|
|||
def token = Jwts.parser().verifyWith(verification).build().parseClaimsJws(jwt)
|
||||
assertEquals([alg: alg.getId()], token.header)
|
||||
assertEquals 'me', token.getPayload().getIssuer()
|
||||
assertEquals 'you', token.getPayload().getAudience()
|
||||
assertEquals 'you', token.getPayload().getAudience().iterator().next()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ class Issue438Test {
|
|||
|
||||
@Test(expected = UnsupportedJwtException /* not IllegalArgumentException */)
|
||||
void testIssue438() {
|
||||
String jws = Jwts.builder().audience('test').signWith(TestKeys.RS256.pair.private).compact()
|
||||
String jws = Jwts.builder().issuer('test').signWith(TestKeys.RS256.pair.private).compact()
|
||||
Jwts.parser().verifyWith(TestKeys.HS256).build().parseClaimsJws(jws)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,6 +45,7 @@ import java.security.interfaces.ECPrivateKey;
|
|||
import java.security.interfaces.ECPublicKey;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.Set;
|
||||
|
||||
import static io.jsonwebtoken.security.Jwks.builder;
|
||||
|
||||
|
@ -183,11 +184,11 @@ public class JavaReadmeTest {
|
|||
.compact();
|
||||
|
||||
// Alice receives and decrypts the compact JWE:
|
||||
String audience = Jwts.parser()
|
||||
Set<String> audience = Jwts.parser()
|
||||
.decryptWith(pair.getPrivate()) // <-- Alice's RSA private key
|
||||
.build().parseClaimsJwe(jwe).getPayload().getAudience();
|
||||
|
||||
assert "Alice".equals(audience);
|
||||
assert audience.contains("Alice");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -231,11 +232,11 @@ public class JavaReadmeTest {
|
|||
.compact();
|
||||
|
||||
// Alice receives and decrypts the compact JWE:
|
||||
String audience = Jwts.parser()
|
||||
Set<String> audience = Jwts.parser()
|
||||
.decryptWith(pair.getPrivate()) // <-- Alice's EC private key
|
||||
.build().parseClaimsJwe(jwe).getPayload().getAudience();
|
||||
|
||||
assert "Alice".equals(audience);
|
||||
assert audience.contains("Alice");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue