Skip to content

Commit 2b00ed1

Browse files
siebenfarbenPhilipp Zormeier
and
Philipp Zormeier
authored
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>
1 parent 82b870e commit 2b00ed1

File tree

7 files changed

+71
-65
lines changed

7 files changed

+71
-65
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ public String compact() {
301301
}
302302

303303
if (payload == null && Collections.isEmpty(claims)) {
304-
throw new IllegalStateException("Either 'payload' or 'claims' must be specified.");
304+
payload = "";
305305
}
306306

307307
if (payload != null && !Collections.isEmpty(claims)) {

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

+17-9
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,11 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException,
257257

258258
Assert.hasText(jwt, "JWT String argument cannot be null or empty.");
259259

260+
if ("..".equals(jwt)) {
261+
String msg = "JWT string '..' is missing a header.";
262+
throw new MalformedJwtException(msg);
263+
}
264+
260265
String base64UrlEncodedHeader = null;
261266
String base64UrlEncodedPayload = null;
262267
String base64UrlEncodedDigest = null;
@@ -293,9 +298,6 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException,
293298
base64UrlEncodedDigest = sb.toString();
294299
}
295300

296-
if (base64UrlEncodedPayload == null) {
297-
throw new MalformedJwtException("JWT string '" + jwt + "' is missing a body/payload.");
298-
}
299301

300302
// =============== Header =================
301303
Header header = null;
@@ -317,15 +319,18 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException,
317319
}
318320

319321
// =============== Body =================
320-
byte[] bytes = base64UrlDecoder.decode(base64UrlEncodedPayload);
321-
if (compressionCodec != null) {
322-
bytes = compressionCodec.decompress(bytes);
322+
String payload = ""; // https://github.com/jwtk/jjwt/pull/540
323+
if (base64UrlEncodedPayload != null) {
324+
byte[] bytes = base64UrlDecoder.decode(base64UrlEncodedPayload);
325+
if (compressionCodec != null) {
326+
bytes = compressionCodec.decompress(bytes);
327+
}
328+
payload = new String(bytes, Strings.UTF_8);
323329
}
324-
String payload = new String(bytes, Strings.UTF_8);
325330

326331
Claims claims = null;
327332

328-
if (payload.charAt(0) == '{' && payload.charAt(payload.length() - 1) == '}') { //likely to be json, parse it:
333+
if (!payload.isEmpty() && payload.charAt(0) == '{' && payload.charAt(payload.length() - 1) == '}') { //likely to be json, parse it:
329334
Map<String, Object> claimsMap = (Map<String, Object>) readValue(payload);
330335
claims = new DefaultClaims(claimsMap);
331336
}
@@ -385,7 +390,10 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException,
385390
Assert.notNull(key, "A signing key must be specified if the specified JWT is digitally signed.");
386391

387392
//re-create the jwt part without the signature. This is what needs to be signed for verification:
388-
String jwtWithoutSignature = base64UrlEncodedHeader + SEPARATOR_CHAR + base64UrlEncodedPayload;
393+
String jwtWithoutSignature = base64UrlEncodedHeader + SEPARATOR_CHAR;
394+
if (base64UrlEncodedPayload != null) {
395+
jwtWithoutSignature += base64UrlEncodedPayload;
396+
}
389397

390398
JwtSignatureValidator validator;
391399
try {

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

-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import java.security.SecureRandom
2828
import static ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE
2929
import static ClaimJwtException.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE
3030
import static org.junit.Assert.*
31-
import static io.jsonwebtoken.DateTestUtils.truncateMillis
3231

3332
class DeprecatedJwtParserTest {
3433

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

+5-18
Original file line numberDiff line numberDiff line change
@@ -167,18 +167,15 @@ class DeprecatedJwtsTest {
167167
Jwts.parser().parse('..')
168168
fail()
169169
} catch (MalformedJwtException e) {
170-
assertEquals e.message, "JWT string '..' is missing a body/payload."
170+
assertEquals e.message, "JWT string '..' is missing a header."
171171
}
172172
}
173173

174174
@Test
175175
void testParseWithHeaderOnly() {
176-
try {
177-
Jwts.parser().parse('foo..')
178-
fail()
179-
} catch (MalformedJwtException e) {
180-
assertEquals e.message, "JWT string 'foo..' is missing a body/payload."
181-
}
176+
String unsecuredJwt = base64Url("{\"alg\":\"none\"}") + ".."
177+
Jwt jwt = Jwts.parser().parse(unsecuredJwt)
178+
assertEquals("none", jwt.getHeader().get("alg"))
182179
}
183180

184181
@Test
@@ -187,17 +184,7 @@ class DeprecatedJwtsTest {
187184
Jwts.parser().parse('..bar')
188185
fail()
189186
} catch (MalformedJwtException e) {
190-
assertEquals e.message, "JWT string '..bar' is missing a body/payload."
191-
}
192-
}
193-
194-
@Test
195-
void testParseWithHeaderAndSignatureOnly() {
196-
try {
197-
Jwts.parser().parse('foo..bar')
198-
fail()
199-
} catch (MalformedJwtException e) {
200-
assertEquals e.message, "JWT string 'foo..bar' is missing a body/payload."
187+
assertEquals e.message, "JWT string has a digest/signature, but the header does not reference a valid signature algorithm."
201188
}
202189
}
203190

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

+40
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ import io.jsonwebtoken.impl.DefaultClock
1919
import io.jsonwebtoken.impl.FixedClock
2020
import io.jsonwebtoken.io.Encoders
2121
import io.jsonwebtoken.lang.Strings
22+
import io.jsonwebtoken.security.Keys
2223
import io.jsonwebtoken.security.SignatureException
2324
import org.junit.Test
2425

26+
import javax.crypto.SecretKey
2527
import javax.crypto.spec.SecretKeySpec
2628
import java.security.SecureRandom
2729

@@ -168,6 +170,44 @@ class JwtParserTest {
168170
assertEquals jwt.body, payload
169171
}
170172

173+
@Test
174+
void testParseEmptyPayload() {
175+
SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256)
176+
177+
String payload = ''
178+
179+
String compact = Jwts.builder().setPayload(payload).signWith(key).compact()
180+
181+
assertTrue Jwts.parserBuilder().build().isSigned(compact)
182+
183+
Jwt<Header, String> jwt = Jwts.parserBuilder().setSigningKey(key).build().parse(compact)
184+
185+
assertEquals payload, jwt.body
186+
}
187+
188+
@Test
189+
void testParseNullPayload() {
190+
SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256)
191+
192+
String compact = Jwts.builder().signWith(key).compact()
193+
194+
assertTrue Jwts.parserBuilder().build().isSigned(compact)
195+
196+
Jwt<Header, String> jwt = Jwts.parserBuilder().setSigningKey(key).build().parse(compact)
197+
198+
assertEquals '', jwt.body
199+
}
200+
201+
@Test
202+
void testParseNullPayloadWithoutKey() {
203+
String compact = Jwts.builder().compact()
204+
205+
Jwt<Header, String> jwt = Jwts.parserBuilder().build().parse(compact)
206+
207+
assertEquals 'none', jwt.header.alg
208+
assertEquals '', jwt.body
209+
}
210+
171211
@Test
172212
void testParseWithExpiredJwt() {
173213

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

+5-18
Original file line numberDiff line numberDiff line change
@@ -167,18 +167,15 @@ class JwtsTest {
167167
Jwts.parserBuilder().build().parse('..')
168168
fail()
169169
} catch (MalformedJwtException e) {
170-
assertEquals e.message, "JWT string '..' is missing a body/payload."
170+
assertEquals e.message, "JWT string '..' is missing a header."
171171
}
172172
}
173173

174174
@Test
175175
void testParseWithHeaderOnly() {
176-
try {
177-
Jwts.parserBuilder().build().parse('foo..')
178-
fail()
179-
} catch (MalformedJwtException e) {
180-
assertEquals e.message, "JWT string 'foo..' is missing a body/payload."
181-
}
176+
String unsecuredJwt = base64Url("{\"alg\":\"none\"}") + ".."
177+
Jwt jwt = Jwts.parserBuilder().build().parse(unsecuredJwt)
178+
assertEquals("none", jwt.getHeader().get("alg"))
182179
}
183180

184181
@Test
@@ -187,17 +184,7 @@ class JwtsTest {
187184
Jwts.parserBuilder().build().parse('..bar')
188185
fail()
189186
} catch (MalformedJwtException e) {
190-
assertEquals e.message, "JWT string '..bar' is missing a body/payload."
191-
}
192-
}
193-
194-
@Test
195-
void testParseWithHeaderAndSignatureOnly() {
196-
try {
197-
Jwts.parserBuilder().build().parse('foo..bar')
198-
fail()
199-
} catch (MalformedJwtException e) {
200-
assertEquals e.message, "JWT string 'foo..bar' is missing a body/payload."
187+
assertEquals e.message, "JWT string has a digest/signature, but the header does not reference a valid signature algorithm."
201188
}
202189
}
203190

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

+3-18
Original file line numberDiff line numberDiff line change
@@ -138,26 +138,11 @@ class DefaultJwtBuilderTest {
138138
assertNull b.claims.foo
139139
}
140140

141-
@Test
142-
void testCompactWithoutBody() {
143-
def b = new DefaultJwtBuilder()
144-
try {
145-
b.compact()
146-
fail()
147-
} catch (IllegalStateException ise) {
148-
assertEquals ise.message, "Either 'payload' or 'claims' must be specified."
149-
}
150-
}
151-
152141
@Test
153142
void testCompactWithoutPayloadOrClaims() {
154-
def b = new DefaultJwtBuilder()
155-
try {
156-
b.compact()
157-
fail()
158-
} catch (IllegalStateException ise) {
159-
assertEquals ise.message, "Either 'payload' or 'claims' must be specified."
160-
}
143+
def compact = new DefaultJwtBuilder().compact()
144+
145+
assertTrue compact.endsWith("..")
161146
}
162147

163148
@Test

0 commit comments

Comments
 (0)