Skip to content

Commit 7ec7dd1

Browse files
authored
Enable JwtParser empty nested algorithm collections. (#1007)
Resolves #996. Allowed the JwtParser to have empty nested algorithm collections, effectively disabling the parser's associated feature: - Clearing the zip() nested collection means parser decompression is disabled - Clearing the sig() nested collection means parser signature verification is disabled (i.e. all JWSs will be unsupported/rejected) - Clearing the enc() or key() nested collections means parser decryption is disabled (i.e. all JWEs will be unsupported/rejected)
1 parent 22690f5 commit 7ec7dd1

File tree

10 files changed

+175
-60
lines changed

10 files changed

+175
-60
lines changed

impl/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717

1818
import io.jsonwebtoken.Header;
1919
import io.jsonwebtoken.impl.lang.CompactMediaTypeIdConverter;
20+
import io.jsonwebtoken.impl.lang.Nameable;
2021
import io.jsonwebtoken.impl.lang.Parameter;
2122
import io.jsonwebtoken.impl.lang.Parameters;
23+
import io.jsonwebtoken.lang.Assert;
2224
import io.jsonwebtoken.lang.Registry;
2325
import io.jsonwebtoken.lang.Strings;
2426

@@ -54,6 +56,11 @@ public String getName() {
5456
return "JWT header";
5557
}
5658

59+
static String nameOf(Header header) {
60+
return Assert.hasText(Assert.isInstanceOf(Nameable.class, header).getName(),
61+
"Header name cannot be null or empty.");
62+
}
63+
5764
@Override
5865
public String getType() {
5966
return get(TYPE);

impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -241,11 +241,11 @@ public class DefaultJwtParser extends AbstractParser<Jwt<?, ?>> implements JwtPa
241241
this.expectedClaims = Jwts.claims().add(expectedClaims);
242242
this.decoder = Assert.notNull(base64UrlDecoder, "base64UrlDecoder cannot be null.");
243243
this.deserializer = Assert.notNull(deserializer, "JSON Deserializer cannot be null.");
244-
this.sigAlgs = new IdLocator<>(DefaultHeader.ALGORITHM, sigAlgs, MISSING_JWS_ALG_MSG);
245-
this.keyAlgs = new IdLocator<>(DefaultHeader.ALGORITHM, keyAlgs, MISSING_JWE_ALG_MSG);
246-
this.encAlgs = new IdLocator<>(DefaultJweHeader.ENCRYPTION_ALGORITHM, encAlgs, MISSING_ENC_MSG);
244+
this.sigAlgs = new IdLocator<>(DefaultHeader.ALGORITHM, sigAlgs, "mac or signature", "signature verification", MISSING_JWS_ALG_MSG);
245+
this.keyAlgs = new IdLocator<>(DefaultHeader.ALGORITHM, keyAlgs, "key management", "decryption", MISSING_JWE_ALG_MSG);
246+
this.encAlgs = new IdLocator<>(DefaultJweHeader.ENCRYPTION_ALGORITHM, encAlgs, "encryption", "decryption", MISSING_ENC_MSG);
247247
this.zipAlgs = compressionCodecResolver != null ? new CompressionCodecLocator(compressionCodecResolver) :
248-
new IdLocator<>(DefaultHeader.COMPRESSION_ALGORITHM, zipAlgs, null);
248+
new IdLocator<>(DefaultHeader.COMPRESSION_ALGORITHM, zipAlgs, "compression", "decompression", null);
249249
}
250250

251251
@Override
@@ -275,7 +275,7 @@ private byte[] verifySignature(final TokenizedJwt tokenized, final JwsHeader jws
275275
algorithm = (SecureDigestAlgorithm<?, Key>) sigAlgs.apply(jwsHeader);
276276
} catch (UnsupportedJwtException e) {
277277
//For backwards compatibility. TODO: remove this try/catch block for 1.0 and let UnsupportedJwtException propagate
278-
String msg = "Unsupported signature algorithm '" + alg + "'";
278+
String msg = "Unsupported signature algorithm '" + alg + "': " + e.getMessage();
279279
throw new SignatureException(msg, e);
280280
}
281281
Assert.stateNotNull(algorithm, "JWS Signature Algorithm cannot be null.");
@@ -459,7 +459,7 @@ private byte[] verifySignature(final TokenizedJwt tokenized, final JwsHeader jws
459459
final boolean payloadBase64UrlEncoded = !(header instanceof JwsHeader) || ((JwsHeader) header).isPayloadEncoded();
460460
if (payloadBase64UrlEncoded) {
461461
// standard encoding, so decode it:
462-
byte[] data = decode(tokenized.getPayload(), "payload");
462+
byte[] data = decode(payloadToken, "payload");
463463
payload = new Payload(data, header.getContentType());
464464
} else {
465465
// The JWT uses the b64 extension, and we already know the parser supports that extension at this point
@@ -493,6 +493,13 @@ private byte[] verifySignature(final TokenizedJwt tokenized, final JwsHeader jws
493493
TokenizedJwe tokenizedJwe = (TokenizedJwe) tokenized;
494494
JweHeader jweHeader = Assert.stateIsInstance(JweHeader.class, header, "Not a JweHeader. ");
495495

496+
// Ensure both an 'alg' and 'enc' header value exists and is supported before spending time/effort
497+
// base64Url-decoding anything:
498+
final AeadAlgorithm encAlg = this.encAlgs.apply(jweHeader);
499+
Assert.stateNotNull(encAlg, "JWE Encryption Algorithm cannot be null.");
500+
@SuppressWarnings("rawtypes") final KeyAlgorithm keyAlg = this.keyAlgs.apply(jweHeader);
501+
Assert.stateNotNull(keyAlg, "JWE Key Algorithm cannot be null.");
502+
496503
byte[] cekBytes = Bytes.EMPTY; //ignored unless using an encrypted key algorithm
497504
CharSequence base64Url = tokenizedJwe.getEncryptedKey();
498505
if (Strings.hasText(base64Url)) {
@@ -529,16 +536,6 @@ private byte[] verifySignature(final TokenizedJwt tokenized, final JwsHeader jws
529536
throw new MalformedJwtException(msg);
530537
}
531538

532-
String enc = jweHeader.getEncryptionAlgorithm();
533-
if (!Strings.hasText(enc)) {
534-
throw new MalformedJwtException(MISSING_ENC_MSG);
535-
}
536-
final AeadAlgorithm encAlg = this.encAlgs.apply(jweHeader);
537-
Assert.stateNotNull(encAlg, "JWE Encryption Algorithm cannot be null.");
538-
539-
@SuppressWarnings("rawtypes") final KeyAlgorithm keyAlg = this.keyAlgs.apply(jweHeader);
540-
Assert.stateNotNull(keyAlg, "JWE Key Algorithm cannot be null.");
541-
542539
Key key = this.keyLocator.locate(jweHeader);
543540
if (key == null) {
544541
String msg = "Cannot decrypt JWE payload: unable to locate key for JWE with header: " + jweHeader;

impl/src/main/java/io/jsonwebtoken/impl/IdLocator.java

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717

1818
import io.jsonwebtoken.Header;
1919
import io.jsonwebtoken.Identifiable;
20-
import io.jsonwebtoken.JweHeader;
21-
import io.jsonwebtoken.JwsHeader;
2220
import io.jsonwebtoken.Locator;
2321
import io.jsonwebtoken.MalformedJwtException;
2422
import io.jsonwebtoken.UnsupportedJwtException;
@@ -31,38 +29,27 @@
3129
public class IdLocator<H extends Header, R extends Identifiable> implements Locator<R>, Function<H, R> {
3230

3331
private final Parameter<String> param;
34-
private final String requiredMsg;
35-
private final boolean valueRequired;
36-
3732
private final Registry<String, R> registry;
33+
private final String algType;
34+
private final String behavior;
35+
private final String requiredMsg;
3836

39-
public IdLocator(Parameter<String> param, Registry<String, R> registry, String requiredExceptionMessage) {
37+
public IdLocator(Parameter<String> param, Registry<String, R> registry, String algType, String behavior, String requiredExceptionMessage) {
4038
this.param = Assert.notNull(param, "Header param cannot be null.");
39+
this.registry = Assert.notNull(registry, "Registry cannot be null.");
40+
this.algType = Assert.hasText(algType, "algType cannot be null or empty.");
41+
this.behavior = Assert.hasText(behavior, "behavior cannot be null or empty.");
4142
this.requiredMsg = Strings.clean(requiredExceptionMessage);
42-
this.valueRequired = Strings.hasText(this.requiredMsg);
43-
Assert.notEmpty(registry, "Registry cannot be null or empty.");
44-
this.registry = registry;
45-
}
46-
47-
private static String type(Header header) {
48-
if (header instanceof JweHeader) {
49-
return "JWE";
50-
} else if (header instanceof JwsHeader) {
51-
return "JWS";
52-
} else {
53-
return "JWT";
54-
}
5543
}
5644

5745
@Override
5846
public R locate(Header header) {
59-
Assert.notNull(header, "Header argument cannot be null.");
6047

6148
Object val = header.get(this.param.getId());
6249
String id = val != null ? val.toString() : null;
6350

6451
if (!Strings.hasText(id)) {
65-
if (this.valueRequired) {
52+
if (this.requiredMsg != null) { // a msg was provided, so the value is required:
6653
throw new MalformedJwtException(requiredMsg);
6754
}
6855
return null; // otherwise header value not required, so short circuit
@@ -71,7 +58,20 @@ public R locate(Header header) {
7158
try {
7259
return registry.forKey(id);
7360
} catch (Exception e) {
74-
String msg = "Unrecognized " + type(header) + " " + this.param + " header value: " + id;
61+
StringBuilder sb = new StringBuilder("Unsupported ")
62+
.append(DefaultHeader.nameOf(header))
63+
.append(" ")
64+
.append(this.param)
65+
.append(" value '").append(id).append("'");
66+
if (this.registry.isEmpty()) {
67+
sb.append(": ")
68+
.append(this.behavior)
69+
.append(" is disabled (no ")
70+
.append(this.algType)
71+
.append(" algorithms have been configured)");
72+
}
73+
sb.append(".");
74+
String msg = sb.toString();
7575
throw new UnsupportedJwtException(msg, e);
7676
}
7777
}

impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultRegistry.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ public class DefaultRegistry<K, V> extends DelegatingMap<K, V, Map<K, V>> implem
2929
private final String qualifiedKeyName;
3030

3131
private static <K, V> Map<K, V> toMap(Collection<? extends V> values, Function<V, K> keyFn) {
32-
Assert.notEmpty(values, "Collection of values may not be null or empty.");
32+
Assert.notNull(values, "Collection of values may not be null.");
3333
Assert.notNull(keyFn, "Key function cannot be null.");
34-
Map<K, V> m = new LinkedHashMap<>(values.size());
34+
Map<K, V> m = new LinkedHashMap<>(Collections.size(values));
3535
for (V value : values) {
3636
K key = Assert.notNull(keyFn.apply(value), "Key function cannot return a null value.");
3737
m.put(key, value);

impl/src/main/java/io/jsonwebtoken/impl/lang/IdRegistry.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public IdRegistry(String name, Collection<T> instances) {
5353

5454
public IdRegistry(String name, Collection<T> instances, boolean caseSensitive) {
5555
super(name, "id",
56-
Assert.notEmpty(instances, "Collection of Identifiable instances may not be null or empty."),
56+
Assert.notNull(instances, "Collection of Identifiable instances may not be null."),
5757
IdRegistry.<T>fn(),
5858
caseSensitive);
5959
}

impl/src/main/java/io/jsonwebtoken/impl/lang/StringRegistry.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public StringRegistry(String name, String keyName, Collection<V> values, Functio
3737
public StringRegistry(String name, String keyName, Collection<V> values, Function<V, String> keyFn, Function<String, String> caseFn) {
3838
super(name, keyName, values, keyFn);
3939
this.CASE_FN = Assert.notNull(caseFn, "Case function cannot be null.");
40-
Map<String, V> m = new LinkedHashMap<>(values().size());
40+
Map<String, V> m = new LinkedHashMap<>(Collections.size(values));
4141
for (V value : values) {
4242
String key = keyFn.apply(value);
4343
key = this.CASE_FN.apply(key);

impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import io.jsonwebtoken.impl.DefaultJwtParser
1919
import io.jsonwebtoken.impl.FixedClock
2020
import io.jsonwebtoken.impl.JwtTokenizer
2121
import io.jsonwebtoken.impl.lang.JwtDateConverter
22+
import io.jsonwebtoken.impl.security.TestKeys
2223
import io.jsonwebtoken.io.Encoders
2324
import io.jsonwebtoken.lang.DateFormats
2425
import io.jsonwebtoken.lang.Strings
@@ -107,7 +108,9 @@ class JwtParserTest {
107108
Jwts.parser().setSigningKey(randomKey()).build().parse(bad)
108109
fail()
109110
} catch (SignatureException se) {
110-
assertEquals se.getMessage(), "Unsupported signature algorithm '$badAlgorithmName'".toString()
111+
String msg = "Unsupported signature algorithm '$badAlgorithmName': " +
112+
"Unsupported JWS header 'alg' (Algorithm) value '$badAlgorithmName'."
113+
assertEquals msg, se.getMessage()
111114
}
112115
}
113116

@@ -1645,4 +1648,115 @@ class JwtParserTest {
16451648
assertEquals 'The JWS header references signature algorithm \'none\' yet the compact JWS string contains a signature. This is not permitted per https://tools.ietf.org/html/rfc7518#section-3.6.', se.message
16461649
}
16471650
}
1651+
1652+
/**
1653+
* Ensures that compression algorithms can be removed completely, thereby disabling compression entirely
1654+
* @see <a href="https://github.com/jwtk/jjwt/issues/996">Issue 996</a>
1655+
* @since 0.12.7
1656+
*/
1657+
@Test
1658+
void testEmptyZipAlgCollection() {
1659+
1660+
// create a compressed JWE first:
1661+
def key = TestKeys.A256GCM
1662+
def jwe = Jwts.builder().claim("hello", "world")
1663+
.compressWith(Jwts.ZIP.DEF)
1664+
.encryptWith(key, Jwts.ENC.A256GCM)
1665+
.compact()
1666+
1667+
//now build a parser with no decompression algs (which should disable decompression)
1668+
def parser = Jwts.parser().zip().clear().and().decryptWith(key).build()
1669+
1670+
//parsing should fail since (de)compression is disabled:
1671+
try {
1672+
parser.parseEncryptedClaims(jwe)
1673+
} catch (UnsupportedJwtException e) {
1674+
String expected = "Unsupported JWE header 'zip' (Compression Algorithm) value 'DEF': " +
1675+
"decompression is disabled (no compression algorithms have been configured)."
1676+
assertEquals expected, e.getMessage()
1677+
}
1678+
}
1679+
1680+
/**
1681+
* Ensures that mac/signature algorithms can be removed completely, thereby disabling JWSs entirely
1682+
* @see <a href="https://github.com/jwtk/jjwt/issues/996">Issue 996</a>
1683+
* @since 0.12.7
1684+
*/
1685+
@Test
1686+
void testEmptySigAlgCollection() {
1687+
1688+
// create a compressed JWE first:
1689+
def key = TestKeys.HS256
1690+
def jws = Jwts.builder().claim("hello", "world")
1691+
.signWith(key, Jwts.SIG.HS256)
1692+
.compact()
1693+
1694+
//now build a parser with no signature algs, which should completely disable signature verification
1695+
def parser = Jwts.parser().sig().clear().and().verifyWith(key).build()
1696+
1697+
//parsing should fail since signature verification is disabled:
1698+
try {
1699+
parser.parseSignedClaims(jws)
1700+
} catch (SignatureException e) {
1701+
String expected = "Unsupported signature algorithm 'HS256': Unsupported JWS header 'alg' (Algorithm) " +
1702+
"value 'HS256': signature verification is disabled (no mac or signature algorithms have been " +
1703+
"configured)."
1704+
assertTrue e.getCause() instanceof UnsupportedJwtException
1705+
assertEquals expected, e.getMessage()
1706+
}
1707+
}
1708+
1709+
/**
1710+
* Ensures that encryption algorithms can be removed completely, thereby disabling JWEs entirely
1711+
* @see <a href="https://github.com/jwtk/jjwt/issues/996">Issue 996</a>
1712+
* @since 0.12.7
1713+
*/
1714+
@Test
1715+
void testEmptyEncAlgCollection() {
1716+
1717+
// create a compressed JWE first:
1718+
def key = TestKeys.A256GCM
1719+
def jwe = Jwts.builder().claim("hello", "world")
1720+
.encryptWith(key, Jwts.ENC.A256GCM)
1721+
.compact()
1722+
1723+
//now build a parser with no encryption algs, which should completely disable decryption
1724+
def parser = Jwts.parser().enc().clear().and().decryptWith(key).build()
1725+
1726+
//parsing should fail since decryption is disabled:
1727+
try {
1728+
parser.parseEncryptedClaims(jwe)
1729+
} catch (UnsupportedJwtException e) {
1730+
String expected = "Unsupported JWE header 'enc' (Encryption Algorithm) value 'A256GCM': " +
1731+
"decryption is disabled (no encryption algorithms have been configured)."
1732+
assertEquals expected, e.getMessage()
1733+
}
1734+
}
1735+
1736+
/**
1737+
* Ensures that key management algorithms can be removed completely, thereby disabling JWEs entirely
1738+
* @see <a href="https://github.com/jwtk/jjwt/issues/996">Issue 996</a>
1739+
* @since 0.12.7
1740+
*/
1741+
@Test
1742+
void testEmptyKeyAlgCollection() {
1743+
1744+
// create a compressed JWE first:
1745+
def key = TestKeys.A256GCM
1746+
def jwe = Jwts.builder().claim("hello", "world")
1747+
.encryptWith(key, Jwts.ENC.A256GCM)
1748+
.compact()
1749+
1750+
//now build a parser with no key management algs, which should completely disable decryption
1751+
def parser = Jwts.parser().key().clear().and().decryptWith(key).build()
1752+
1753+
//parsing should fail since key management is disabled:
1754+
try {
1755+
parser.parseEncryptedClaims(jwe)
1756+
} catch (UnsupportedJwtException e) {
1757+
String expected = "Unsupported JWE header 'alg' (Algorithm) value 'dir': decryption is disabled " +
1758+
"(no key management algorithms have been configured)."
1759+
assertEquals expected, e.getMessage()
1760+
}
1761+
}
16481762
}

impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,7 @@ import io.jsonwebtoken.impl.io.Streams
2222
import io.jsonwebtoken.impl.lang.Bytes
2323
import io.jsonwebtoken.impl.lang.Services
2424
import io.jsonwebtoken.impl.security.*
25-
import io.jsonwebtoken.io.CompressionAlgorithm
26-
import io.jsonwebtoken.io.Decoders
27-
import io.jsonwebtoken.io.Deserializer
28-
import io.jsonwebtoken.io.Encoders
29-
import io.jsonwebtoken.io.Serializer
25+
import io.jsonwebtoken.io.*
3026
import io.jsonwebtoken.lang.Strings
3127
import io.jsonwebtoken.security.*
3228
import org.junit.Test
@@ -987,7 +983,7 @@ class JwtsTest {
987983
Jwts.parser().build().parseEncryptedClaims(compact)
988984
fail()
989985
} catch (UnsupportedJwtException e) {
990-
String expected = "Unrecognized JWE 'enc' (Encryption Algorithm) header value: foo"
986+
String expected = "Unsupported JWE header 'enc' (Encryption Algorithm) value 'foo'."
991987
assertEquals expected, e.getMessage()
992988
}
993989
}
@@ -1007,7 +1003,7 @@ class JwtsTest {
10071003
Jwts.parser().build().parseEncryptedClaims(compact)
10081004
fail()
10091005
} catch (UnsupportedJwtException e) {
1010-
String expected = "Unrecognized JWE 'alg' (Algorithm) header value: bar"
1006+
String expected = "Unsupported JWE header 'alg' (Algorithm) value 'bar'."
10111007
assertEquals expected, e.getMessage()
10121008
}
10131009
}
@@ -1025,7 +1021,8 @@ class JwtsTest {
10251021
Jwts.parser().build().parseSignedClaims(compact)
10261022
fail()
10271023
} catch (io.jsonwebtoken.security.SignatureException e) {
1028-
String expected = "Unsupported signature algorithm 'bar'"
1024+
String expected = "Unsupported signature algorithm 'bar': " +
1025+
"Unsupported JWS header 'alg' (Algorithm) value 'bar'."
10291026
assertEquals expected, e.getMessage()
10301027
}
10311028
}

0 commit comments

Comments
 (0)