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)) {
throw new IllegalStateException("Either 'payload' or 'claims' must be specified.");
payload = "";
}
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.");
if ("..".equals(jwt)) {
String msg = "JWT string '..' is missing a header.";
throw new MalformedJwtException(msg);
}
String base64UrlEncodedHeader = null;
String base64UrlEncodedPayload = null;
String base64UrlEncodedDigest = null;
@ -293,9 +298,6 @@ public class DefaultJwtParser implements JwtParser {
base64UrlEncodedDigest = sb.toString();
}
if (base64UrlEncodedPayload == null) {
throw new MalformedJwtException("JWT string '" + jwt + "' is missing a body/payload.");
}
// =============== Header =================
Header header = null;
@ -317,15 +319,18 @@ public class DefaultJwtParser implements JwtParser {
}
// =============== Body =================
String payload = ""; // https://github.com/jwtk/jjwt/pull/540
if (base64UrlEncodedPayload != null) {
byte[] bytes = base64UrlDecoder.decode(base64UrlEncodedPayload);
if (compressionCodec != null) {
bytes = compressionCodec.decompress(bytes);
}
String payload = new String(bytes, Strings.UTF_8);
payload = new String(bytes, Strings.UTF_8);
}
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);
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.");
//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;
try {

View File

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

View File

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

View File

@ -19,9 +19,11 @@ import io.jsonwebtoken.impl.DefaultClock
import io.jsonwebtoken.impl.FixedClock
import io.jsonwebtoken.io.Encoders
import io.jsonwebtoken.lang.Strings
import io.jsonwebtoken.security.Keys
import io.jsonwebtoken.security.SignatureException
import org.junit.Test
import javax.crypto.SecretKey
import javax.crypto.spec.SecretKeySpec
import java.security.SecureRandom
@ -168,6 +170,44 @@ class JwtParserTest {
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
void testParseWithExpiredJwt() {

View File

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

View File

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