mirror of https://github.com/jwtk/jjwt.git
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:
parent
82b870e283
commit
2b00ed1819
|
@ -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)) {
|
||||||
|
|
|
@ -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 =================
|
||||||
|
String payload = ""; // https://github.com/jwtk/jjwt/pull/540
|
||||||
|
if (base64UrlEncodedPayload != null) {
|
||||||
byte[] bytes = base64UrlDecoder.decode(base64UrlEncodedPayload);
|
byte[] bytes = base64UrlDecoder.decode(base64UrlEncodedPayload);
|
||||||
if (compressionCodec != null) {
|
if (compressionCodec != null) {
|
||||||
bytes = compressionCodec.decompress(bytes);
|
bytes = compressionCodec.decompress(bytes);
|
||||||
}
|
}
|
||||||
String payload = new String(bytes, Strings.UTF_8);
|
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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
||||||
|
|
|
@ -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."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
||||||
|
|
|
@ -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."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue