Skip to content

Commit

Permalink
Audience claim string arrays (#819)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
lhazlewood authored Sep 9, 2023
1 parent 524429e commit ffbe947
Show file tree
Hide file tree
Showing 24 changed files with 567 additions and 114 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down
41 changes: 29 additions & 12 deletions api/src/main/java/io/jsonwebtoken/Claims.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

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>.
Expand All @@ -38,25 +39,39 @@
*/
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";

/**
Expand All @@ -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">
Expand Down Expand Up @@ -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();

/**
Expand All @@ -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}
*/
Expand Down
48 changes: 42 additions & 6 deletions api/src/main/java/io/jsonwebtoken/ClaimsMutator.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package io.jsonwebtoken;

import java.util.Collection;
import java.util.Date;

/**
Expand Down Expand Up @@ -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.
Expand All @@ -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 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 the JWT {@code aud} value or {@code null} to remove the property from the JSON map.
* @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) 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(String aud);
T audience(Collection<String> aud);

/**
* Sets the JWT <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.4">
Expand Down
4 changes: 2 additions & 2 deletions api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
18 changes: 18 additions & 0 deletions api/src/main/java/io/jsonwebtoken/lang/Collections.java
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,24 @@ public static <T> List<T> of(T... elements) {
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.
*
Expand Down
15 changes: 15 additions & 0 deletions api/src/main/java/io/jsonwebtoken/security/PrivateKeyBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
52 changes: 52 additions & 0 deletions api/src/test/groovy/io/jsonwebtoken/lang/CollectionsTest.groovy
Original file line number Diff line number Diff line change
@@ -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
}
}
}
9 changes: 7 additions & 2 deletions impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import java.util.Date;
import java.util.Map;
import java.util.Set;

public class DefaultClaims extends FieldMap implements Claims {

Expand All @@ -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");
Expand All @@ -51,6 +52,10 @@ protected DefaultClaims() { // visibility for testing
super(FIELDS);
}

public DefaultClaims(FieldMap m) {
super(m.FIELDS, m);
}

public DefaultClaims(Map<String, ?> map) {
super(FIELDS, map);
}
Expand All @@ -71,7 +76,7 @@ public String getSubject() {
}

@Override
public String getAudience() {
public Set<String> getAudience() {
return get(AUDIENCE);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public DefaultJweHeaderMutator(DefaultJweHeaderMutator<?> src) {
// 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();
}
Expand Down
16 changes: 15 additions & 1 deletion impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
import java.security.Provider;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.util.Collection;
import java.util.Date;
import java.util.Map;

Expand Down Expand Up @@ -393,7 +394,14 @@ public JwtBuilder subject(String sub) {

@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
Expand All @@ -402,6 +410,12 @@ public JwtBuilder audience(String aud) {
return this;
}

@Override
public JwtBuilder audience(Collection<String> aud) {
this.claimsBuilder.audience(aud);
return this;
}

@Override
public JwtBuilder setExpiration(Date exp) {
return expiration(exp);
Expand Down
Loading

0 comments on commit ffbe947

Please sign in to comment.