Skip to content

Commit

Permalink
Closes #328
Browse files Browse the repository at this point in the history
- Ensured Parser worked with CharSequence, InputStream and Reader, not just String
- Changed Deserializer#deserialize(InputStream) to deserialize(Reader)
- JwtParser now extends from Parser to support these additional methods.
  • Loading branch information
lhazlewood committed Sep 30, 2023
1 parent 12abcae commit 0653123
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 52 deletions.
3 changes: 2 additions & 1 deletion api/src/main/java/io/jsonwebtoken/JwtParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package io.jsonwebtoken;

import io.jsonwebtoken.io.Parser;
import io.jsonwebtoken.security.SecurityException;
import io.jsonwebtoken.security.SignatureException;

Expand All @@ -25,7 +26,7 @@
*
* @since 0.1
*/
public interface JwtParser {
public interface JwtParser extends Parser<Jwt<?, ?>> {

/**
* Returns {@code true} if the specified JWT compact string represents a signed JWT (aka a 'JWS'), {@code false}
Expand Down
23 changes: 15 additions & 8 deletions impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
import io.jsonwebtoken.ProtectedHeader;
import io.jsonwebtoken.SigningKeyResolver;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.impl.io.AbstractParser;
import io.jsonwebtoken.impl.io.CharSequenceReader;
import io.jsonwebtoken.impl.io.JsonObjectDeserializer;
import io.jsonwebtoken.impl.io.Streams;
import io.jsonwebtoken.impl.io.UncloseableInputStream;
Expand Down Expand Up @@ -92,7 +94,7 @@
import java.util.Set;

@SuppressWarnings("unchecked")
public class DefaultJwtParser implements JwtParser {
public class DefaultJwtParser extends AbstractParser<Jwt<?, ?>> implements JwtParser {

static final char SEPARATOR_CHAR = '.';

Expand Down Expand Up @@ -250,11 +252,11 @@ public class DefaultJwtParser implements JwtParser {

@Override
public boolean isSigned(String compact) {
if (compact == null) {
if (!Strings.hasText(compact)) {
return false;
}
try {
final TokenizedJwt tokenized = jwtTokenizer.tokenize(compact);
final TokenizedJwt tokenized = jwtTokenizer.tokenize(new CharSequenceReader(compact));
return !(tokenized instanceof TokenizedJwe) && Strings.hasText(tokenized.getDigest());
} catch (MalformedJwtException e) {
return false;
Expand Down Expand Up @@ -356,16 +358,21 @@ private void verifySignature(final TokenizedJwt tokenized, final JwsHeader jwsHe
}
}

@Override
public Jwt<?, ?> parse(Reader reader) {
Assert.notNull(reader, "Reader cannot be null.");
return parse(reader, Payload.EMPTY);
}

@Override
public Jwt<?, ?> parse(String compact) {
CharBuffer buffer = Strings.wrap(compact); // so compact.subsequence calls don't add new Strings on the heap
return parse(buffer, Payload.EMPTY);
return parse((CharSequence) compact);
}

private Jwt<?, ?> parse(CharSequence compact, Payload unencodedPayload)
private Jwt<?, ?> parse(Reader compact, Payload unencodedPayload)
throws ExpiredJwtException, MalformedJwtException, SignatureException {

Assert.hasText(compact, "JWT String cannot be null or empty.");
//Assert.hasText(compact, "JWT String cannot be null or empty.");
Assert.stateNotNull(unencodedPayload, "internal error: unencodedPayload is null.");

final TokenizedJwt tokenized = jwtTokenizer.tokenize(compact);
Expand Down Expand Up @@ -777,7 +784,7 @@ private <T> T parse(String compact, Payload unencodedPayload, JwtHandler<T> hand
Assert.notNull(handler, "JwtHandler argument cannot be null.");
Assert.hasText(compact, "JWT String argument cannot be null or empty.");

Jwt<?, ?> jwt = parse(compact, unencodedPayload);
Jwt<?, ?> jwt = parse(new CharSequenceReader(compact), unencodedPayload);

if (jwt instanceof Jws) {
Jws<?> jws = (Jws<?>) jwt;
Expand Down
88 changes: 56 additions & 32 deletions impl/src/main/java/io/jsonwebtoken/impl/JwtTokenizer.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,62 +16,84 @@
package io.jsonwebtoken.impl;

import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.impl.io.Streams;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Strings;

import java.io.IOException;
import java.io.Reader;

public class JwtTokenizer {

static final char DELIMITER = '.';

private static final String DELIM_ERR_MSG_PREFIX = "Invalid compact JWT string: Compact JWSs must contain " +
"exactly 2 period characters, and compact JWEs must contain exactly 4. Found: ";

private static int read(Reader r, char[] buf) {
try {
return r.read(buf);
} catch (IOException e) {
String msg = "Unable to read compact JWT: " + e.getMessage();
throw new MalformedJwtException(msg, e);
}
}

@SuppressWarnings("unchecked")
public <T extends TokenizedJwt> T tokenize(CharSequence jwt) {
public <T extends TokenizedJwt> T tokenize(Reader reader) {

Assert.hasText(jwt, "Argument cannot be null or empty.");
Assert.notNull(reader, "Reader argument cannot be null.");

CharSequence protectedHeader = Strings.EMPTY; //Both JWS and JWE
CharSequence body = Strings.EMPTY; //JWS payload or JWE Ciphertext
CharSequence encryptedKey = Strings.EMPTY; //JWE only
CharSequence iv = Strings.EMPTY; //JWE only
CharSequence digest; //JWS Signature or JWE AAD Tag
CharSequence digest = Strings.EMPTY; //JWS Signature or JWE AAD Tag

int delimiterCount = 0;
int start = 0;
char[] buf = new char[4096];
int len = 0;
StringBuilder sb = new StringBuilder(4096);
while (len != Streams.EOF) {

for (int i = 0; i < jwt.length(); i++) {
len = read(reader, buf);

char c = jwt.charAt(i);
for (int i = 0; i < len; i++) {

if (Character.isWhitespace(c)) {
String msg = "Compact JWT strings may not contain whitespace.";
throw new MalformedJwtException(msg);
}
char c = buf[i];

if (c == DELIMITER) {

CharSequence token = jwt.subSequence(start, i);
start = i + 1;

switch (delimiterCount) {
case 0:
protectedHeader = token;
break;
case 1:
body = token; //for JWS
encryptedKey = token; //for JWE
break;
case 2:
body = Strings.EMPTY; //clear out value set for JWS
iv = token;
break;
case 3:
body = token;
break;
if (Character.isWhitespace(c)) {
String msg = "Compact JWT strings may not contain whitespace.";
throw new MalformedJwtException(msg);
}

delimiterCount++;
if (c == DELIMITER) {

CharSequence seq = Strings.clean(sb);
String token = seq != null ? seq.toString() : Strings.EMPTY;

switch (delimiterCount) {
case 0:
protectedHeader = token;
break;
case 1:
body = token; //for JWS
encryptedKey = token; //for JWE
break;
case 2:
body = Strings.EMPTY; //clear out value set for JWS
iv = token;
break;
case 3:
body = token;
break;
}

delimiterCount++;
sb.setLength(0);
} else {
sb.append(c);
}
}
}

Expand All @@ -80,7 +102,9 @@ public <T extends TokenizedJwt> T tokenize(CharSequence jwt) {
throw new MalformedJwtException(msg);
}

digest = jwt.subSequence(start, jwt.length());
if (sb.length() > 0) {
digest = sb.toString();
}

if (delimiterCount == 2) {
return (T) new DefaultTokenizedJwt(protectedHeader, body, digest);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package io.jsonwebtoken.impl.io;

import io.jsonwebtoken.io.Parser;
import io.jsonwebtoken.lang.Assert;

import java.io.InputStream;
import java.io.Reader;
Expand All @@ -24,17 +25,20 @@ public abstract class AbstractParser<T> implements Parser<T> {

@Override
public final T parse(CharSequence input) {
Assert.hasText(input, "CharSequence cannot be null or empty.");
return parse(input, 0, input.length());
}

@Override
public T parse(CharSequence input, int start, int end) {
Assert.hasText(input, "CharSequence cannot be null or empty.");
Reader reader = new CharSequenceReader(input, start, end);
return parse(reader);
}

@Override
public final T parse(InputStream in) {
Assert.notNull(in, "InputStream cannot be null.");
Reader reader = Streams.reader(in);
return parse(reader);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public ConvertingParser(Function<Reader, Map<String, ?>> deserializer, Converter

@Override
public final T parse(Reader reader) {
Assert.notNull(reader, "Reader cannot be null.");
Map<String, ?> m = this.deserializer.apply(reader);
return this.converter.applyFrom(m);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@ class DefaultJwtBuilderTest {
def jwt = builder.audience().single(audienceSingleString).compact()
// can't use the parser here to validate because it coerces the string value into an array automatically,
// so we need to check the raw payload:
def encoded = new JwtTokenizer().tokenize(jwt).getPayload()
def encoded = new JwtTokenizer().tokenize(Streams.reader(jwt)).getPayload()
byte[] bytes = Decoders.BASE64URL.decode(encoded)
def claims = deser(bytes)

Expand All @@ -618,7 +618,7 @@ class DefaultJwtBuilderTest {
def jwt = builder.audience().single(first).audience().single(second).compact()
// can't use the parser here to validate because it coerces the string value into an array automatically,
// so we need to check the raw payload:
def encoded = new JwtTokenizer().tokenize(jwt).getPayload()
def encoded = new JwtTokenizer().tokenize(Streams.reader(jwt)).getPayload()
byte[] bytes = Decoders.BASE64URL.decode(encoded)
def claims = deser(bytes)

Expand Down Expand Up @@ -754,7 +754,7 @@ class DefaultJwtBuilderTest {

// can't use the parser here to validate because it coerces the string value into an array automatically,
// so we need to check the raw payload:
def encoded = new JwtTokenizer().tokenize(jwt).getPayload()
def encoded = new JwtTokenizer().tokenize(Streams.reader(jwt)).getPayload()
byte[] bytes = Decoders.BASE64URL.decode(encoded)
def claims = Services.loadFirst(Deserializer).deserialize(Streams.reader(bytes))

Expand Down Expand Up @@ -791,7 +791,7 @@ class DefaultJwtBuilderTest {

// can't use the parser here to validate because it coerces the string value into an array automatically,
// so we need to check the raw payload:
def encoded = new JwtTokenizer().tokenize(jwt).getPayload()
def encoded = new JwtTokenizer().tokenize(Streams.reader(jwt)).getPayload()
byte[] bytes = Decoders.BASE64URL.decode(encoded)
def claims = deser(bytes)

Expand Down
38 changes: 31 additions & 7 deletions impl/src/test/groovy/io/jsonwebtoken/impl/JwtTokenizerTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package io.jsonwebtoken.impl

import io.jsonwebtoken.MalformedJwtException
import io.jsonwebtoken.impl.io.Streams
import org.junit.Before
import org.junit.Test

Expand All @@ -32,40 +33,63 @@ class JwtTokenizerTest {
tokenizer = new JwtTokenizer()
}

private def tokenize(CharSequence s) {
return tokenizer.tokenize(Streams.reader(s))
}

@Test(expected = MalformedJwtException)
void testParseWithWhitespaceInBase64UrlHeader() {
def input = 'header .body.signature'
tokenizer.tokenize(input)
tokenize(input)
}

@Test(expected = MalformedJwtException)
void testParseWithWhitespaceInBase64UrlBody() {
def input = 'header. body.signature'
tokenizer.tokenize(input)
tokenize(input)
}

@Test(expected = MalformedJwtException)
void testParseWithWhitespaceInBase64UrlSignature() {
def input = 'header.body. signature'
tokenizer.tokenize(input)
tokenize(input)
}

@Test(expected = MalformedJwtException)
void testParseWithWhitespaceInBase64UrlJweBody() {
def input = 'header.encryptedKey.initializationVector. body.authenticationTag'
tokenizer.tokenize(input)
tokenize(input)
}

@Test(expected = MalformedJwtException)
void testParseWithWhitespaceInBase64UrlJweTag() {
def input = 'header.encryptedKey.initializationVector.body. authenticationTag'
tokenizer.tokenize(input)
tokenize(input)
}

@Test
void readerExceptionResultsInMalformedJwtException() {
IOException ioe = new IOException('foo')
def reader = new StringReader('hello') {
@Override
int read(char[] chars) throws IOException {
throw ioe
}
}
try {
JwtTokenizer.read(reader, new char[0])
fail()
} catch (MalformedJwtException expected) {
String msg = 'Unable to read compact JWT: foo'
assertEquals msg, expected.message
assertSame ioe, expected.cause
}
}

@Test
void testEmptyJws() {
def input = CharBuffer.wrap('header..digest'.toCharArray())
def t = tokenizer.tokenize(input)
def t = tokenize(input)
assertTrue t instanceof TokenizedJwt
assertFalse t instanceof TokenizedJwe
assertEquals 'header', t.getProtected().toString()
Expand All @@ -78,7 +102,7 @@ class JwtTokenizerTest {

def input = 'header.encryptedKey.initializationVector.body.authenticationTag'

def t = tokenizer.tokenize(input)
def t = tokenize(input)

assertNotNull t
assertTrue t instanceof TokenizedJwe
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ class DefaultJwkParserBuilderTest {
KipQrCW9CGZH3T4ubpnoTKLDYJ_fF3_rJt"
}''')

@Test(expected = IllegalArgumentException)
void parseNull() {
Jwks.parser().build().parse((CharSequence)null)
}

@Test(expected = IllegalArgumentException)
void parseEmpty() {
Jwks.parser().build().parse(Strings.EMPTY)
}

@Test
void testStaticFactoryMethod() {
assertTrue Jwks.parser() instanceof DefaultJwkParserBuilder
Expand Down

0 comments on commit 0653123

Please sign in to comment.