Add DefaultJwtParser functionality to parse JWSs with empty body. (#540)

* Add DefaultJwtParser functionality to parse JWSs with empty body.

* Review Fix: Change allowEmptyBody(boolean) to requirePayload(boolean). Set payloadRequired true for each require*() method in JwtParser and JwtParserBuilder.

* Add missing ImmutableJwtParserTest.

* Review changes: Moving to solution without payload requirement flag.

* Review changes: Allow empty Jwt payload

* Remove unused imports

Co-authored-by: Philipp Zormeier <philipp.zormeier@thoughtworks.com>
This commit is contained in:
Philipp Zormeier 2020-06-08 20:07:10 +02:00 committed by GitHub
parent 82b870e283
commit 2b00ed1819
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 71 additions and 65 deletions

View File

@ -301,7 +301,7 @@ public class DefaultJwtBuilder implements JwtBuilder {
} }
if (payload == null && Collections.isEmpty(claims)) { if (payload == null && Collections.isEmpty(claims)) {
throw new IllegalStateException("Either 'payload' or 'claims' must be specified."); payload = "";
} }
if (payload != null && !Collections.isEmpty(claims)) { if (payload != null && !Collections.isEmpty(claims)) {

View File

@ -257,6 +257,11 @@ public class DefaultJwtParser implements JwtParser {
Assert.hasText(jwt, "JWT String argument cannot be null or empty."); Assert.hasText(jwt, "JWT String argument cannot be null or empty.");
if ("..".equals(jwt)) {
String msg = "JWT string '..' is missing a header.";
throw new MalformedJwtException(msg);
}
String base64UrlEncodedHeader = null; String base64UrlEncodedHeader = null;
String base64UrlEncodedPayload = null; String base64UrlEncodedPayload = null;
String base64UrlEncodedDigest = null; String base64UrlEncodedDigest = null;
@ -293,9 +298,6 @@ public class DefaultJwtParser implements JwtParser {
base64UrlEncodedDigest = sb.toString(); base64UrlEncodedDigest = sb.toString();
} }
if (base64UrlEncodedPayload == null) {
throw new MalformedJwtException("JWT string '" + jwt + "' is missing a body/payload.");
}
// =============== Header ================= // =============== Header =================
Header header = null; Header header = null;
@ -317,15 +319,18 @@ public class DefaultJwtParser implements JwtParser {
} }
// =============== Body ================= // =============== Body =================
byte[] bytes = base64UrlDecoder.decode(base64UrlEncodedPayload); String payload = ""; // https://github.com/jwtk/jjwt/pull/540
if (compressionCodec != null) { if (base64UrlEncodedPayload != null) {
bytes = compressionCodec.decompress(bytes); byte[] bytes = base64UrlDecoder.decode(base64UrlEncodedPayload);
if (compressionCodec != null) {
bytes = compressionCodec.decompress(bytes);
}
payload = new String(bytes, Strings.UTF_8);
} }
String payload = new String(bytes, Strings.UTF_8);
Claims claims = null; Claims claims = null;
if (payload.charAt(0) == '{' && payload.charAt(payload.length() - 1) == '}') { //likely to be json, parse it: if (!payload.isEmpty() && payload.charAt(0) == '{' && payload.charAt(payload.length() - 1) == '}') { //likely to be json, parse it:
Map<String, Object> claimsMap = (Map<String, Object>) readValue(payload); Map<String, Object> claimsMap = (Map<String, Object>) readValue(payload);
claims = new DefaultClaims(claimsMap); claims = new DefaultClaims(claimsMap);
} }
@ -385,7 +390,10 @@ public class DefaultJwtParser implements JwtParser {
Assert.notNull(key, "A signing key must be specified if the specified JWT is digitally signed."); Assert.notNull(key, "A signing key must be specified if the specified JWT is digitally signed.");
//re-create the jwt part without the signature. This is what needs to be signed for verification: //re-create the jwt part without the signature. This is what needs to be signed for verification:
String jwtWithoutSignature = base64UrlEncodedHeader + SEPARATOR_CHAR + base64UrlEncodedPayload; String jwtWithoutSignature = base64UrlEncodedHeader + SEPARATOR_CHAR;
if (base64UrlEncodedPayload != null) {
jwtWithoutSignature += base64UrlEncodedPayload;
}
JwtSignatureValidator validator; JwtSignatureValidator validator;
try { try {

View File

@ -28,7 +28,6 @@ import java.security.SecureRandom
import static ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE import static ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE
import static ClaimJwtException.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE import static ClaimJwtException.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE
import static org.junit.Assert.* import static org.junit.Assert.*
import static io.jsonwebtoken.DateTestUtils.truncateMillis
class DeprecatedJwtParserTest { class DeprecatedJwtParserTest {

View File

@ -167,18 +167,15 @@ class DeprecatedJwtsTest {
Jwts.parser().parse('..') Jwts.parser().parse('..')
fail() fail()
} catch (MalformedJwtException e) { } catch (MalformedJwtException e) {
assertEquals e.message, "JWT string '..' is missing a body/payload." assertEquals e.message, "JWT string '..' is missing a header."
} }
} }
@Test @Test
void testParseWithHeaderOnly() { void testParseWithHeaderOnly() {
try { String unsecuredJwt = base64Url("{\"alg\":\"none\"}") + ".."
Jwts.parser().parse('foo..') Jwt jwt = Jwts.parser().parse(unsecuredJwt)
fail() assertEquals("none", jwt.getHeader().get("alg"))
} catch (MalformedJwtException e) {
assertEquals e.message, "JWT string 'foo..' is missing a body/payload."
}
} }
@Test @Test
@ -187,17 +184,7 @@ class DeprecatedJwtsTest {
Jwts.parser().parse('..bar') Jwts.parser().parse('..bar')
fail() fail()
} catch (MalformedJwtException e) { } catch (MalformedJwtException e) {
assertEquals e.message, "JWT string '..bar' is missing a body/payload." assertEquals e.message, "JWT string has a digest/signature, but the header does not reference a valid signature algorithm."
}
}
@Test
void testParseWithHeaderAndSignatureOnly() {
try {
Jwts.parser().parse('foo..bar')
fail()
} catch (MalformedJwtException e) {
assertEquals e.message, "JWT string 'foo..bar' is missing a body/payload."
} }
} }

View File

@ -19,9 +19,11 @@ import io.jsonwebtoken.impl.DefaultClock
import io.jsonwebtoken.impl.FixedClock import io.jsonwebtoken.impl.FixedClock
import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.io.Encoders
import io.jsonwebtoken.lang.Strings import io.jsonwebtoken.lang.Strings
import io.jsonwebtoken.security.Keys
import io.jsonwebtoken.security.SignatureException import io.jsonwebtoken.security.SignatureException
import org.junit.Test import org.junit.Test
import javax.crypto.SecretKey
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
import java.security.SecureRandom import java.security.SecureRandom
@ -168,6 +170,44 @@ class JwtParserTest {
assertEquals jwt.body, payload assertEquals jwt.body, payload
} }
@Test
void testParseEmptyPayload() {
SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256)
String payload = ''
String compact = Jwts.builder().setPayload(payload).signWith(key).compact()
assertTrue Jwts.parserBuilder().build().isSigned(compact)
Jwt<Header, String> jwt = Jwts.parserBuilder().setSigningKey(key).build().parse(compact)
assertEquals payload, jwt.body
}
@Test
void testParseNullPayload() {
SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256)
String compact = Jwts.builder().signWith(key).compact()
assertTrue Jwts.parserBuilder().build().isSigned(compact)
Jwt<Header, String> jwt = Jwts.parserBuilder().setSigningKey(key).build().parse(compact)
assertEquals '', jwt.body
}
@Test
void testParseNullPayloadWithoutKey() {
String compact = Jwts.builder().compact()
Jwt<Header, String> jwt = Jwts.parserBuilder().build().parse(compact)
assertEquals 'none', jwt.header.alg
assertEquals '', jwt.body
}
@Test @Test
void testParseWithExpiredJwt() { void testParseWithExpiredJwt() {

View File

@ -167,18 +167,15 @@ class JwtsTest {
Jwts.parserBuilder().build().parse('..') Jwts.parserBuilder().build().parse('..')
fail() fail()
} catch (MalformedJwtException e) { } catch (MalformedJwtException e) {
assertEquals e.message, "JWT string '..' is missing a body/payload." assertEquals e.message, "JWT string '..' is missing a header."
} }
} }
@Test @Test
void testParseWithHeaderOnly() { void testParseWithHeaderOnly() {
try { String unsecuredJwt = base64Url("{\"alg\":\"none\"}") + ".."
Jwts.parserBuilder().build().parse('foo..') Jwt jwt = Jwts.parserBuilder().build().parse(unsecuredJwt)
fail() assertEquals("none", jwt.getHeader().get("alg"))
} catch (MalformedJwtException e) {
assertEquals e.message, "JWT string 'foo..' is missing a body/payload."
}
} }
@Test @Test
@ -187,17 +184,7 @@ class JwtsTest {
Jwts.parserBuilder().build().parse('..bar') Jwts.parserBuilder().build().parse('..bar')
fail() fail()
} catch (MalformedJwtException e) { } catch (MalformedJwtException e) {
assertEquals e.message, "JWT string '..bar' is missing a body/payload." assertEquals e.message, "JWT string has a digest/signature, but the header does not reference a valid signature algorithm."
}
}
@Test
void testParseWithHeaderAndSignatureOnly() {
try {
Jwts.parserBuilder().build().parse('foo..bar')
fail()
} catch (MalformedJwtException e) {
assertEquals e.message, "JWT string 'foo..bar' is missing a body/payload."
} }
} }

View File

@ -138,26 +138,11 @@ class DefaultJwtBuilderTest {
assertNull b.claims.foo assertNull b.claims.foo
} }
@Test
void testCompactWithoutBody() {
def b = new DefaultJwtBuilder()
try {
b.compact()
fail()
} catch (IllegalStateException ise) {
assertEquals ise.message, "Either 'payload' or 'claims' must be specified."
}
}
@Test @Test
void testCompactWithoutPayloadOrClaims() { void testCompactWithoutPayloadOrClaims() {
def b = new DefaultJwtBuilder() def compact = new DefaultJwtBuilder().compact()
try {
b.compact() assertTrue compact.endsWith("..")
fail()
} catch (IllegalStateException ise) {
assertEquals ise.message, "Either 'payload' or 'claims' must be specified."
}
} }
@Test @Test