From 8f49666a40abc73b57ffb998f56750a6a7744c17 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Sat, 12 Sep 2015 01:09:44 -0400 Subject: [PATCH 01/19] Enforce arbitrary claim expectations when parsing --- README.md | 8 +- .../io/jsonwebtoken/ClaimJwtException.java | 3 + .../jsonwebtoken/IncorrectClaimException.java | 32 +++++ .../jsonwebtoken/InvalidClaimException.java | 50 +++++++ src/main/java/io/jsonwebtoken/JwtParser.java | 17 +++ .../jsonwebtoken/MissingClaimException.java | 32 +++++ .../jsonwebtoken/impl/DefaultJwtParser.java | 47 +++++++ .../IncorrectClaimExceptionTest.groovy | 46 +++++++ .../InvalidClaimExceptionTest.groovy | 46 +++++++ .../io/jsonwebtoken/JwtParserTest.groovy | 125 ++++++++++++++++++ .../MissingClaimExceptionTest.groovy | 46 +++++++ 11 files changed, 450 insertions(+), 2 deletions(-) create mode 100644 src/main/java/io/jsonwebtoken/IncorrectClaimException.java create mode 100644 src/main/java/io/jsonwebtoken/InvalidClaimException.java create mode 100644 src/main/java/io/jsonwebtoken/MissingClaimException.java create mode 100644 src/test/groovy/io/jsonwebtoken/IncorrectClaimExceptionTest.groovy create mode 100644 src/test/groovy/io/jsonwebtoken/InvalidClaimExceptionTest.groovy create mode 100644 src/test/groovy/io/jsonwebtoken/MissingClaimExceptionTest.groovy diff --git a/README.md b/README.md index d09f55e2..d49c82b6 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Maven: io.jsonwebtoken jjwt - 0.5.1 + 0.6 ``` @@ -24,7 +24,7 @@ Gradle: ```groovy dependencies { - compile 'io.jsonwebtoken:jjwt:0.5.1' + compile 'io.jsonwebtoken:jjwt:0.6' } ``` @@ -99,6 +99,10 @@ These feature sets will be implemented in a future release when possible. Commu ## Release Notes +### 0.6 + +- Added the ability to set expectations when parsing a JWT which enforces a particular claim having a particular value + ### 0.5.1 - Minor [bug](https://github.com/jwtk/jjwt/issues/31) fix [release](https://github.com/jwtk/jjwt/issues?q=milestone%3A0.5.1+is%3Aclosed) that ensures correct Base64 padding in Android runtimes. diff --git a/src/main/java/io/jsonwebtoken/ClaimJwtException.java b/src/main/java/io/jsonwebtoken/ClaimJwtException.java index 89059b2b..bb7c81fd 100644 --- a/src/main/java/io/jsonwebtoken/ClaimJwtException.java +++ b/src/main/java/io/jsonwebtoken/ClaimJwtException.java @@ -22,6 +22,9 @@ package io.jsonwebtoken; */ public abstract class ClaimJwtException extends JwtException { + public static final String INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE = "Expected %s claim to be: %s, but was: %s."; + public static final String MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE = "Expected %s claim to be: %s, but was not present in the JWT claims."; + private final Header header; private final Claims claims; diff --git a/src/main/java/io/jsonwebtoken/IncorrectClaimException.java b/src/main/java/io/jsonwebtoken/IncorrectClaimException.java new file mode 100644 index 00000000..bf63cd5a --- /dev/null +++ b/src/main/java/io/jsonwebtoken/IncorrectClaimException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2015 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken; + +/** + * IncorrectClaimException is a subclass of the {@link InvalidClaimException} that is thrown after it is found that an + * expected claim has a value that is not expected. + * + * @since 0.6 + */ +public class IncorrectClaimException extends InvalidClaimException { + public IncorrectClaimException(Header header, Claims claims, String message) { + super(header, claims, message); + } + + public IncorrectClaimException(Header header, Claims claims, String message, Throwable cause) { + super(header, claims, message, cause); + } +} diff --git a/src/main/java/io/jsonwebtoken/InvalidClaimException.java b/src/main/java/io/jsonwebtoken/InvalidClaimException.java new file mode 100644 index 00000000..9706a0b3 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/InvalidClaimException.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2015 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken; + +/** + * InvalidClaimException is a subclass of the {@link ClaimJwtException} that is thrown after a validation of an JTW claim failed. + * + * @since 0.6 + */ +public class InvalidClaimException extends ClaimJwtException { + private String claimName; + private Object claimValue; + + protected InvalidClaimException(Header header, Claims claims, String message) { + super(header, claims, message); + } + + protected InvalidClaimException(Header header, Claims claims, String message, Throwable cause) { + super(header, claims, message, cause); + } + + public String getClaimName() { + return claimName; + } + + public void setClaimName(String claimName) { + this.claimName = claimName; + } + + public Object getClaimValue() { + return claimValue; + } + + public void setClaimValue(Object claimValue) { + this.claimValue = claimValue; + } +} diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index f4b190ba..c26da433 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -26,6 +26,23 @@ public interface JwtParser { public static final char SEPARATOR_CHAR = '.'; + /** + * Sets an expected value for any given claim name. + * + * If an expectation is set for a particular claim name and the JWT being parsed does not have that claim set, + * a {@Link MissingClaimException} will be thrown. + * + * If an expectation is set for a particular claim name and the JWT being parsed has a value that is different than + * the expected value, a {@link IncorrectClaimException} will be thrown. + * + * If either {@code claimName} is null or empty or {@code value} is null, the expectation is simply ignored. + * + * @param claimName + * @param value + * @return the parser for method chaining. + */ + JwtParser expect(String claimName, Object value); + /** * Sets the signing key used to verify any discovered JWS digital signature. If the specified JWT string is not * a JWS (no signature), this key is not used. diff --git a/src/main/java/io/jsonwebtoken/MissingClaimException.java b/src/main/java/io/jsonwebtoken/MissingClaimException.java new file mode 100644 index 00000000..d453df57 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/MissingClaimException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2015 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken; + +/** + * MissingClaimException is a subclass of the {@link InvalidClaimException} that is thrown after it is found that an + * expected claim is missing. + * + * @since 0.6 + */ +public class MissingClaimException extends InvalidClaimException { + public MissingClaimException(Header header, Claims claims, String message) { + super(header, claims, message); + } + + public MissingClaimException(Header header, Claims claims, String message, Throwable cause) { + super(header, claims, message, cause); + } +} diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index ba888e04..36142292 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -16,11 +16,15 @@ package io.jsonwebtoken.impl; import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.ClaimJwtException; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Header; +import io.jsonwebtoken.IncorrectClaimException; +import io.jsonwebtoken.InvalidClaimException; import io.jsonwebtoken.Jws; import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.MissingClaimException; import io.jsonwebtoken.SigningKeyResolver; import io.jsonwebtoken.Jwt; import io.jsonwebtoken.JwtHandler; @@ -42,6 +46,7 @@ import java.io.IOException; import java.security.Key; import java.text.SimpleDateFormat; import java.util.Date; +import java.util.LinkedHashMap; import java.util.Map; @SuppressWarnings("unchecked") @@ -58,6 +63,17 @@ public class DefaultJwtParser implements JwtParser { private SigningKeyResolver signingKeyResolver; + Map expectedClaims = new LinkedHashMap(); + + @Override + public JwtParser expect(String claimName, Object value) { + if (claimName != null && claimName.length() > 0 && value != null) { + expectedClaims.put(claimName, value); + } + + return this; + } + @Override public JwtParser setSigningKey(byte[] key) { Assert.notEmpty(key, "signing key cannot be null or empty."); @@ -298,6 +314,8 @@ public class DefaultJwtParser implements JwtParser { throw new PrematureJwtException(header, claims, msg); } } + + validateExpectedClaims(header, claims); } Object body = claims != null ? claims : payload; @@ -309,6 +327,35 @@ public class DefaultJwtParser implements JwtParser { } } + private void validateExpectedClaims(Header header, Claims claims) { + for (String expectedClaimName : expectedClaims.keySet()) { + Object expectedClaimValue = expectedClaims.get(expectedClaimName); + Object actualClaimValue = claims.get(expectedClaimName); + InvalidClaimException invalidClaimException = null; + + if (actualClaimValue == null) { + String msg = String.format( + ClaimJwtException.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, + expectedClaimName, expectedClaimValue + ); + invalidClaimException = new MissingClaimException(header, claims, msg); + } + else if (!expectedClaimValue.equals(actualClaimValue)) { + String msg = String.format( + ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, + expectedClaimName, expectedClaimValue, actualClaimValue + ); + invalidClaimException = new IncorrectClaimException(header, claims, msg); + } + + if (invalidClaimException != null) { + invalidClaimException.setClaimName(expectedClaimName); + invalidClaimException.setClaimValue(expectedClaimValue); + throw invalidClaimException; + } + } + } + /* * @since 0.5 mostly to allow testing overrides */ diff --git a/src/test/groovy/io/jsonwebtoken/IncorrectClaimExceptionTest.groovy b/src/test/groovy/io/jsonwebtoken/IncorrectClaimExceptionTest.groovy new file mode 100644 index 00000000..3a3062ab --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/IncorrectClaimExceptionTest.groovy @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2015 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken + +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertSame + +class IncorrectClaimExceptionTest { + + @Test + void testOverloadedConstructor() { + def header = Jwts.header() + def claims = Jwts.claims() + def msg = 'foo' + def cause = new NullPointerException() + + def claimName = 'cName' + def claimValue = 'cValue' + + def ex = new IncorrectClaimException(header, claims, msg, cause) + ex.setClaimName(claimName) + ex.setClaimValue(claimValue) + + assertSame ex.header, header + assertSame ex.claims, claims + assertEquals ex.message, msg + assertSame ex.cause, cause + assertEquals ex.claimName, claimName + assertEquals ex.claimValue, claimValue + } +} diff --git a/src/test/groovy/io/jsonwebtoken/InvalidClaimExceptionTest.groovy b/src/test/groovy/io/jsonwebtoken/InvalidClaimExceptionTest.groovy new file mode 100644 index 00000000..5209a041 --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/InvalidClaimExceptionTest.groovy @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2015 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken + +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertSame + +class InvalidClaimExceptionTest { + + @Test + void testOverloadedConstructor() { + def header = Jwts.header() + def claims = Jwts.claims() + def msg = 'foo' + def cause = new NullPointerException() + + def claimName = 'cName' + def claimValue = 'cValue' + + def ex = new InvalidClaimException(header, claims, msg, cause) + ex.setClaimName(claimName) + ex.setClaimValue(claimValue) + + assertSame ex.header, header + assertSame ex.claims, claims + assertEquals ex.message, msg + assertSame ex.cause, cause + assertEquals ex.claimName, claimName + assertEquals ex.claimValue, claimValue + } +} diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index a831c4b1..40c4986c 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -723,4 +723,129 @@ class JwtParserTest { 'method or, for HMAC algorithms, the resolveSigningKeyBytes(JwsHeader, String) method.' } } + + @Test + void testParseExpectIgnoreNullClaimName() { + def expectedClaimValue = 'A Most Awesome Claim Value' + + byte[] key = randomKey() + + // not setting expected claim name in JWT + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setIssuer('Dummy'). + compact() + + // expecting null claim name, but with value + Jwt jwt = Jwts.parser().setSigningKey(key). + expect(null, expectedClaimValue). + parseClaimsJws(compact) + + assertEquals jwt.getBody().getIssuer(), 'Dummy' + } + + @Test + void testParseExpectIgnoreEmptyClaimName() { + def expectedClaimValue = 'A Most Awesome Claim Value' + + byte[] key = randomKey() + + // not setting expected claim name in JWT + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setIssuer('Dummy'). + compact() + + // expecting null claim name, but with value + Jwt jwt = Jwts.parser().setSigningKey(key). + expect("", expectedClaimValue). + parseClaimsJws(compact) + + assertEquals jwt.getBody().getIssuer(), 'Dummy' + } + + @Test + void testParseExpectIgnoreNullClaimValue() { + def expectedClaimName = 'A Most Awesome Claim Name' + + byte[] key = randomKey() + + // not setting expected claim name in JWT + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setIssuer('Dummy'). + compact() + + // expecting claim name, but with null value + Jwt jwt = Jwts.parser().setSigningKey(key). + expect(expectedClaimName, null). + parseClaimsJws(compact) + + assertEquals jwt.getBody().getIssuer(), 'Dummy' + } + + @Test + void testParseExpectGeneric_Success() { + def expectedClaimName = 'A Most Awesome Claim Name' + def expectedClaimValue = 'A Most Awesome Claim Value' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + claim(expectedClaimName, expectedClaimValue). + compact() + + Jwt jwt = Jwts.parser().setSigningKey(key). + expect(expectedClaimName, expectedClaimValue). + parseClaimsJws(compact) + + assertEquals jwt.getBody().get(expectedClaimName), expectedClaimValue + } + + @Test + void testParseExpectGeneric_Incorrect_Fail() { + def goodClaimName = 'A Most Awesome Claim Name' + def goodClaimValue = 'A Most Awesome Claim Value' + + def badClaimValue = 'A Most Bogus Claim Value' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + claim(goodClaimName, badClaimValue). + compact() + + try { + Jwts.parser().setSigningKey(key). + expect(goodClaimName, goodClaimValue). + parseClaimsJws(compact) + fail() + } catch (IncorrectClaimException e) { + assertEquals( + String.format(ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, goodClaimName, goodClaimValue, badClaimValue), + e.getMessage() + ) + } + } + + @Test + void testParseExpectedGeneric_Missing_Fail() { + def claimName = 'A Most Awesome Claim Name' + def claimValue = 'A Most Awesome Claim Value' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setIssuer('Dummy'). + compact() + + try { + Jwt jwt = Jwts.parser().setSigningKey(key). + expect(claimName, claimValue). + parseClaimsJws(compact) + fail() + } catch (MissingClaimException e) { + assertEquals( + String.format(ClaimJwtException.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, claimName, claimValue), + e.getMessage() + ) + } + } } diff --git a/src/test/groovy/io/jsonwebtoken/MissingClaimExceptionTest.groovy b/src/test/groovy/io/jsonwebtoken/MissingClaimExceptionTest.groovy new file mode 100644 index 00000000..36658df6 --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/MissingClaimExceptionTest.groovy @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2015 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken + +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertSame + +class MissingClaimExceptionTest { + + @Test + void testOverloadedConstructor() { + def header = Jwts.header() + def claims = Jwts.claims() + def msg = 'foo' + def cause = new NullPointerException() + + def claimName = 'cName' + def claimValue = 'cValue' + + def ex = new MissingClaimException(header, claims, msg, cause) + ex.setClaimName(claimName) + ex.setClaimValue(claimValue) + + assertSame ex.header, header + assertSame ex.claims, claims + assertEquals ex.message, msg + assertSame ex.cause, cause + assertEquals ex.claimName, claimName + assertEquals ex.claimValue, claimValue + } +} From 0fab5504cdf6f179b5ab3f450afec7c5aa644eec Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Sat, 12 Sep 2015 02:53:57 -0400 Subject: [PATCH 02/19] Added expectIssuedAt convenience method. --- src/main/java/io/jsonwebtoken/JwtParser.java | 9 +++ .../jsonwebtoken/impl/DefaultJwtParser.java | 24 +++++- .../io/jsonwebtoken/JwtParserTest.groovy | 73 ++++++++++++++++++- 3 files changed, 100 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index c26da433..fbf2a81f 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -16,6 +16,7 @@ package io.jsonwebtoken; import java.security.Key; +import java.util.Date; /** * A parser for reading JWT strings, used to convert them into a {@link Jwt} object representing the expanded JWT. @@ -26,6 +27,14 @@ public interface JwtParser { public static final char SEPARATOR_CHAR = '.'; + /** + * Sets an expected value for the issuedAt claim. + * + * @param issuedAt + * @return the parser for method chaining. + */ + JwtParser expectIssuedAt(Date issuedAt); + /** * Sets an expected value for any given claim name. * diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 36142292..26f17f0e 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -46,7 +46,6 @@ import java.io.IOException; import java.security.Key; import java.text.SimpleDateFormat; import java.util.Date; -import java.util.LinkedHashMap; import java.util.Map; @SuppressWarnings("unchecked") @@ -63,7 +62,16 @@ public class DefaultJwtParser implements JwtParser { private SigningKeyResolver signingKeyResolver; - Map expectedClaims = new LinkedHashMap(); + Claims expectedClaims = new DefaultClaims(); + + @Override + public JwtParser expectIssuedAt(Date issuedAt) { + if (issuedAt != null) { + expectedClaims.setIssuedAt(issuedAt); + } + + return this; + } @Override public JwtParser expect(String claimName, Object value) { @@ -329,8 +337,16 @@ public class DefaultJwtParser implements JwtParser { private void validateExpectedClaims(Header header, Claims claims) { for (String expectedClaimName : expectedClaims.keySet()) { - Object expectedClaimValue = expectedClaims.get(expectedClaimName); - Object actualClaimValue = claims.get(expectedClaimName); + Object expectedClaimValue = null; + Object actualClaimValue = null; + if (Claims.ISSUED_AT.equals(expectedClaimName)) { + expectedClaimValue = expectedClaims.getIssuedAt(); + actualClaimValue = claims.getIssuedAt(); + } else { + expectedClaimValue = expectedClaims.get(expectedClaimName); + actualClaimValue = claims.get(expectedClaimName); + } + InvalidClaimException invalidClaimException = null; if (actualClaimValue == null) { diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 40c4986c..c5726dab 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -22,6 +22,8 @@ import javax.crypto.spec.SecretKeySpec import java.security.SecureRandom import static org.junit.Assert.* +import static ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE +import static ClaimJwtException.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE class JwtParserTest { @@ -819,7 +821,7 @@ class JwtParserTest { fail() } catch (IncorrectClaimException e) { assertEquals( - String.format(ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, goodClaimName, goodClaimValue, badClaimValue), + String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, goodClaimName, goodClaimValue, badClaimValue), e.getMessage() ) } @@ -843,7 +845,74 @@ class JwtParserTest { fail() } catch (MissingClaimException e) { assertEquals( - String.format(ClaimJwtException.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, claimName, claimValue), + String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, claimName, claimValue), + e.getMessage() + ) + } + } + + @Test + void testParseExpectIssuedAt_Success() { + def issuedAt = new Date(System.currentTimeMillis()) + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setIssuedAt(issuedAt). + compact() + + Jwt jwt = Jwts.parser().setSigningKey(key). + expectIssuedAt(issuedAt). + parseClaimsJws(compact) + + // system converts to seconds (lopping off millis precision), then returns millis + def issuedAtMillis = ((long)issuedAt.getTime() / 1000) * 1000 + + assertEquals jwt.getBody().getIssuedAt().getTime(), issuedAtMillis + } + + @Test + void testParseExpectIssuedAt_Incorrect_Fail() { + def goodIssuedAt = new Date(System.currentTimeMillis()) + def badIssuedAt = new Date(System.currentTimeMillis() - 10000) + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setIssuedAt(badIssuedAt). + compact() + + try { + Jwts.parser().setSigningKey(key). + expectIssuedAt(goodIssuedAt). + parseClaimsJws(compact) + fail() + } catch(IncorrectClaimException e) { + assertEquals( + String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.ISSUED_AT, goodIssuedAt, badIssuedAt), + e.getMessage() + ) + } + } + + @Test + void testParseExpectIssuedAt_Missing_Fail() { + def issuedAt = new Date(System.currentTimeMillis() - 10000) + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setSubject("Dummy"). + compact() + + try { + Jwts.parser().setSigningKey(key). + expectIssuedAt(issuedAt). + parseClaimsJws(compact) + fail() + } catch(MissingClaimException e) { + assertEquals( + String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.ISSUED_AT, issuedAt), e.getMessage() ) } From 056dc819e2cc44deb8925fe534b2a23d20e3db4c Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Sat, 12 Sep 2015 03:08:03 -0400 Subject: [PATCH 03/19] Added expectIssuer convenience method. --- src/main/java/io/jsonwebtoken/JwtParser.java | 8 +++ .../jsonwebtoken/impl/DefaultJwtParser.java | 15 ++++- .../io/jsonwebtoken/JwtParserTest.groovy | 64 +++++++++++++++++++ 3 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index fbf2a81f..f9c2e54e 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -27,6 +27,14 @@ public interface JwtParser { public static final char SEPARATOR_CHAR = '.'; + /** + * Sets an expected value for the issuer claim. + * + * @param issuer + * @return the parser for method chaining. + */ + JwtParser expectIssuer(String issuer); + /** * Sets an expected value for the issuedAt claim. * diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 26f17f0e..be898304 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -73,6 +73,13 @@ public class DefaultJwtParser implements JwtParser { return this; } + @Override + public JwtParser expectIssuer(String issuer) { + expect(Claims.ISSUER, issuer); + + return this; + } + @Override public JwtParser expect(String claimName, Object value) { if (claimName != null && claimName.length() > 0 && value != null) { @@ -337,8 +344,12 @@ public class DefaultJwtParser implements JwtParser { private void validateExpectedClaims(Header header, Claims claims) { for (String expectedClaimName : expectedClaims.keySet()) { - Object expectedClaimValue = null; - Object actualClaimValue = null; + Object expectedClaimValue; + Object actualClaimValue; + + // since issued at is a date, call the specific method + // other methods deal with strings and the more + // general method can be used if (Claims.ISSUED_AT.equals(expectedClaimName)) { expectedClaimValue = expectedClaims.getIssuedAt(); actualClaimValue = claims.getIssuedAt(); diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index c5726dab..da3dbb21 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -917,4 +917,68 @@ class JwtParserTest { ) } } + + @Test + void testParseExpectIssuer_Success() { + def issuer = 'A Most Awesome Issuer' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setIssuer(issuer). + compact() + + Jwt jwt = Jwts.parser().setSigningKey(key). + expectIssuer(issuer). + parseClaimsJws(compact) + + assertEquals jwt.getBody().getIssuer(), issuer + } + + @Test + void testParseExpectIssuer_Incorrect_Fail() { + def goodIssuer = 'A Most Awesome Issuer' + def badIssuer = 'A Most Bogus Issuer' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setIssuer(badIssuer). + compact() + + try { + Jwts.parser().setSigningKey(key). + expectIssuer(goodIssuer). + parseClaimsJws(compact) + fail() + } catch(IncorrectClaimException e) { + assertEquals( + String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.ISSUER, goodIssuer, badIssuer), + e.getMessage() + ) + } + } + + @Test + void testParseExpectIssuer_Missing_Fail() { + def issuer = 'A Most Awesome Issuer' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setId('id'). + compact() + + try { + Jwts.parser().setSigningKey(key). + expectIssuer(issuer). + parseClaimsJws(compact) + fail() + } catch(MissingClaimException e) { + assertEquals( + String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.ISSUER, issuer), + e.getMessage() + ) + } + } } From fd04a357cb4e0ae90d991c94924335032bc9dbe5 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Sat, 12 Sep 2015 03:32:56 -0400 Subject: [PATCH 04/19] Added expectAudience convenience method. --- src/main/java/io/jsonwebtoken/JwtParser.java | 8 +++ .../jsonwebtoken/impl/DefaultJwtParser.java | 7 ++ .../io/jsonwebtoken/JwtParserTest.groovy | 65 +++++++++++++++++++ 3 files changed, 80 insertions(+) diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index f9c2e54e..9f6906c7 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -27,6 +27,14 @@ public interface JwtParser { public static final char SEPARATOR_CHAR = '.'; + /** + * Sets an expected value for the audience claim. + * + * @param audience + * @return the parser for method chaining. + */ + JwtParser expectAudience(String audience); + /** * Sets an expected value for the issuer claim. * diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index be898304..08664d09 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -80,6 +80,13 @@ public class DefaultJwtParser implements JwtParser { return this; } + @Override + public JwtParser expectAudience(String audience) { + expect(Claims.AUDIENCE, audience); + + return this; + } + @Override public JwtParser expect(String claimName, Object value) { if (claimName != null && claimName.length() > 0 && value != null) { diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index da3dbb21..7cd1d88d 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -981,4 +981,69 @@ class JwtParserTest { ) } } + + @Test + void testParseExpectAudience_Success() { + def audience = 'A Most Awesome Audience' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setAudience(audience). + compact() + + Jwt jwt = Jwts.parser().setSigningKey(key). + expectAudience(audience). + parseClaimsJws(compact) + + assertEquals jwt.getBody().getAudience(), audience + } + + @Test + void testParseExpectAudience_Incorrect_Fail() { + def goodAudience = 'A Most Awesome Audience' + def badAudience = 'A Most Bogus Audience' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setAudience(badAudience). + compact() + + try { + Jwts.parser().setSigningKey(key). + expectAudience(goodAudience). + parseClaimsJws(compact) + fail() + } catch(IncorrectClaimException e) { + assertEquals( + String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.AUDIENCE, goodAudience, badAudience), + e.getMessage() + ) + } + } + + @Test + void testParseExpectAudience_Missing_Fail() { + def audience = 'A Most Awesome audience' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setId('id'). + compact() + + try { + Jwts.parser().setSigningKey(key). + expectAudience(audience). + parseClaimsJws(compact) + fail() + } catch(MissingClaimException e) { + assertEquals( + String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.AUDIENCE, audience), + e.getMessage() + ) + } + } + } From f3c8f10f32d89a447ead2311536a5de90b5a02eb Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Sat, 12 Sep 2015 03:40:20 -0400 Subject: [PATCH 05/19] Added expectSubject convenience method. --- src/main/java/io/jsonwebtoken/JwtParser.java | 8 +++ .../jsonwebtoken/impl/DefaultJwtParser.java | 7 +++ .../io/jsonwebtoken/JwtParserTest.groovy | 63 +++++++++++++++++++ 3 files changed, 78 insertions(+) diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index 9f6906c7..c74ec9e6 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -27,6 +27,14 @@ public interface JwtParser { public static final char SEPARATOR_CHAR = '.'; + /** + * Sets an expected value for the subject claim. + * + * @param subject + * @return the parser for method chaining. + */ + JwtParser expectSubject(String subject); + /** * Sets an expected value for the audience claim. * diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 08664d09..a251ca8e 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -87,6 +87,13 @@ public class DefaultJwtParser implements JwtParser { return this; } + @Override + public JwtParser expectSubject(String subject) { + expect(Claims.SUBJECT, subject); + + return this; + } + @Override public JwtParser expect(String claimName, Object value) { if (claimName != null && claimName.length() > 0 && value != null) { diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 7cd1d88d..c6116842 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -1046,4 +1046,67 @@ class JwtParserTest { } } + @Test + void testParseExpectSubject_Success() { + def subject = 'A Most Awesome Subject' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setSubject(subject). + compact() + + Jwt jwt = Jwts.parser().setSigningKey(key). + expectSubject(subject). + parseClaimsJws(compact) + + assertEquals jwt.getBody().getSubject(), subject + } + + @Test + void testParseExpectSubject_Incorrect_Fail() { + def goodSubject = 'A Most Awesome Subject' + def badSubject = 'A Most Bogus Subject' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setSubject(badSubject). + compact() + + try { + Jwts.parser().setSigningKey(key). + expectSubject(goodSubject). + parseClaimsJws(compact) + fail() + } catch(IncorrectClaimException e) { + assertEquals( + String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.SUBJECT, goodSubject, badSubject), + e.getMessage() + ) + } + } + + @Test + void testParseExpectSubject_Missing_Fail() { + def subject = 'A Most Awesome Subject' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setId('id'). + compact() + + try { + Jwts.parser().setSigningKey(key). + expectSubject(subject). + parseClaimsJws(compact) + fail() + } catch(MissingClaimException e) { + assertEquals( + String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.SUBJECT, subject), + e.getMessage() + ) + } + } } From f2e620e36bff02804f99a20b5190cafd42bb22b3 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Sat, 12 Sep 2015 03:51:00 -0400 Subject: [PATCH 06/19] Added expectId convenience method. --- src/main/java/io/jsonwebtoken/JwtParser.java | 8 +++ .../jsonwebtoken/impl/DefaultJwtParser.java | 7 ++ .../io/jsonwebtoken/JwtParserTest.groovy | 64 +++++++++++++++++++ 3 files changed, 79 insertions(+) diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index c74ec9e6..3e0cff47 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -27,6 +27,14 @@ public interface JwtParser { public static final char SEPARATOR_CHAR = '.'; + /** + * Sets an expected value for the jti claim. + * + * @param id + * @return the parser for method chaining. + */ + JwtParser expectId(String id); + /** * Sets an expected value for the subject claim. * diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index a251ca8e..30588e1f 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -94,6 +94,13 @@ public class DefaultJwtParser implements JwtParser { return this; } + @Override + public JwtParser expectId(String id) { + expect(Claims.ID, id); + + return this; + } + @Override public JwtParser expect(String claimName, Object value) { if (claimName != null && claimName.length() > 0 && value != null) { diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index c6116842..a5b74580 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -1109,4 +1109,68 @@ class JwtParserTest { ) } } + + @Test + void testParseExpectId_Success() { + def id = 'A Most Awesome id' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setId(id). + compact() + + Jwt jwt = Jwts.parser().setSigningKey(key). + expectId(id). + parseClaimsJws(compact) + + assertEquals jwt.getBody().getId(), id + } + + @Test + void testParseExpectId_Incorrect_Fail() { + def goodId = 'A Most Awesome Id' + def badId = 'A Most Bogus Id' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setId(badId). + compact() + + try { + Jwts.parser().setSigningKey(key). + expectId(goodId). + parseClaimsJws(compact) + fail() + } catch(IncorrectClaimException e) { + assertEquals( + String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.ID, goodId, badId), + e.getMessage() + ) + } + } + + @Test + void testParseExpectId_Missing_Fail() { + def id = 'A Most Awesome Id' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setIssuer('me'). + compact() + + try { + Jwts.parser().setSigningKey(key). + expectId(id). + parseClaimsJws(compact) + fail() + } catch(MissingClaimException e) { + assertEquals( + String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.ID, id), + e.getMessage() + ) + } + } } From 5ecaacde5a61b8f1c1f7ad711ab51720bb095aa6 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Wed, 23 Sep 2015 01:12:54 -0400 Subject: [PATCH 07/19] Don't allow empty or null claimName or null value for claim expectations. --- .../jsonwebtoken/impl/DefaultJwtParser.java | 6 +- .../io/jsonwebtoken/JwtParserTest.groovy | 60 ++++++++++++------- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 30588e1f..9e9c5ee9 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -103,9 +103,9 @@ public class DefaultJwtParser implements JwtParser { @Override public JwtParser expect(String claimName, Object value) { - if (claimName != null && claimName.length() > 0 && value != null) { - expectedClaims.put(claimName, value); - } + Assert.hasText(claimName, "claim name cannot be null or empty."); + Assert.notNull(value, "The value cannot be null for claim name: " + claimName); + expectedClaims.put(claimName, value); return this; } diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index a5b74580..ea0bda6a 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -727,7 +727,7 @@ class JwtParserTest { } @Test - void testParseExpectIgnoreNullClaimName() { + void testParseExpectDontAllowNullClaimName() { def expectedClaimValue = 'A Most Awesome Claim Value' byte[] key = randomKey() @@ -737,35 +737,47 @@ class JwtParserTest { setIssuer('Dummy'). compact() - // expecting null claim name, but with value - Jwt jwt = Jwts.parser().setSigningKey(key). - expect(null, expectedClaimValue). - parseClaimsJws(compact) - - assertEquals jwt.getBody().getIssuer(), 'Dummy' + try { + // expecting null claim name, but with value + Jwt jwt = Jwts.parser().setSigningKey(key). + expect(null, expectedClaimValue). + parseClaimsJws(compact) + fail() + } catch (IllegalArgumentException e) { + assertEquals( + "claim name cannot be null or empty.", + e.getMessage() + ) + } } @Test - void testParseExpectIgnoreEmptyClaimName() { + void testParseExpectDontAllowEmptyClaimName() { def expectedClaimValue = 'A Most Awesome Claim Value' byte[] key = randomKey() // not setting expected claim name in JWT String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). - setIssuer('Dummy'). - compact() + setIssuer('Dummy'). + compact() - // expecting null claim name, but with value - Jwt jwt = Jwts.parser().setSigningKey(key). + try { + // expecting null claim name, but with value + Jwt jwt = Jwts.parser().setSigningKey(key). expect("", expectedClaimValue). parseClaimsJws(compact) - - assertEquals jwt.getBody().getIssuer(), 'Dummy' + fail() + } catch (IllegalArgumentException e) { + assertEquals( + "claim name cannot be null or empty.", + e.getMessage() + ) + } } @Test - void testParseExpectIgnoreNullClaimValue() { + void testParseExpectDontAllowNullClaimValue() { def expectedClaimName = 'A Most Awesome Claim Name' byte[] key = randomKey() @@ -775,12 +787,18 @@ class JwtParserTest { setIssuer('Dummy'). compact() - // expecting claim name, but with null value - Jwt jwt = Jwts.parser().setSigningKey(key). - expect(expectedClaimName, null). - parseClaimsJws(compact) - - assertEquals jwt.getBody().getIssuer(), 'Dummy' + try { + // expecting claim name, but with null value + Jwt jwt = Jwts.parser().setSigningKey(key). + expect(expectedClaimName, null). + parseClaimsJws(compact) + fail() + } catch (IllegalArgumentException e) { + assertEquals( + "The value cannot be null for claim name: " + expectedClaimName, + e.getMessage() + ) + } } @Test From 62ccd16748f3c89b41dfdd62b8978c5ade772e29 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Wed, 23 Sep 2015 01:31:17 -0400 Subject: [PATCH 08/19] Call underlying delegate methods for expected claims. --- .../java/io/jsonwebtoken/impl/DefaultJwtParser.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 9e9c5ee9..228a40c0 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -66,37 +66,35 @@ public class DefaultJwtParser implements JwtParser { @Override public JwtParser expectIssuedAt(Date issuedAt) { - if (issuedAt != null) { - expectedClaims.setIssuedAt(issuedAt); - } + expectedClaims.setIssuedAt(issuedAt); return this; } @Override public JwtParser expectIssuer(String issuer) { - expect(Claims.ISSUER, issuer); + expectedClaims.setIssuer(issuer); return this; } @Override public JwtParser expectAudience(String audience) { - expect(Claims.AUDIENCE, audience); + expectedClaims.setAudience(audience); return this; } @Override public JwtParser expectSubject(String subject) { - expect(Claims.SUBJECT, subject); + expectedClaims.setSubject(subject); return this; } @Override public JwtParser expectId(String id) { - expect(Claims.ID, id); + expectedClaims.setId(id); return this; } From 5dd95b67553123a8d8d4c62b9137604078675599 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Wed, 23 Sep 2015 03:18:10 -0400 Subject: [PATCH 09/19] Refactored validateExpectedClaims --- .../jsonwebtoken/impl/DefaultJwtParser.java | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 228a40c0..5a815c76 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -363,18 +363,32 @@ public class DefaultJwtParser implements JwtParser { private void validateExpectedClaims(Header header, Claims claims) { for (String expectedClaimName : expectedClaims.keySet()) { - Object expectedClaimValue; - Object actualClaimValue; - // since issued at is a date, call the specific method - // other methods deal with strings and the more - // general method can be used + // this will be overridden if one of the default claims is used + Object expectedClaimValue = expectedClaims.get(expectedClaimName); + Object actualClaimValue = claims.get(expectedClaimName); + if (Claims.ISSUED_AT.equals(expectedClaimName)) { expectedClaimValue = expectedClaims.getIssuedAt(); actualClaimValue = claims.getIssuedAt(); - } else { - expectedClaimValue = expectedClaims.get(expectedClaimName); - actualClaimValue = claims.get(expectedClaimName); + } else if (Claims.AUDIENCE.equals(expectedClaimName)) { + expectedClaimValue = expectedClaims.getAudience(); + actualClaimValue = claims.getAudience(); + } else if (Claims.ISSUER.equals(expectedClaimName)) { + expectedClaimValue = expectedClaims.getIssuer(); + actualClaimValue = claims.getIssuer(); + } else if (Claims.SUBJECT.equals(expectedClaimName)) { + expectedClaimValue = expectedClaims.getSubject(); + actualClaimValue = claims.getSubject(); + } else if (Claims.EXPIRATION.equals(expectedClaimName)) { + expectedClaimValue = expectedClaims.getExpiration(); + actualClaimValue = claims.getExpiration(); + } else if (Claims.NOT_BEFORE.equals(expectedClaimName)) { + expectedClaimValue = expectedClaims.getNotBefore(); + actualClaimValue = claims.getNotBefore(); + } else if (Claims.ID.equals(expectedClaimName)) { + expectedClaimValue = expectedClaims.getId(); + actualClaimValue = claims.getId(); } InvalidClaimException invalidClaimException = null; @@ -385,8 +399,7 @@ public class DefaultJwtParser implements JwtParser { expectedClaimName, expectedClaimValue ); invalidClaimException = new MissingClaimException(header, claims, msg); - } - else if (!expectedClaimValue.equals(actualClaimValue)) { + } else if (!expectedClaimValue.equals(actualClaimValue)) { String msg = String.format( ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, expectedClaimName, expectedClaimValue, actualClaimValue From 72acd649c7092d3d2d2d23c4cb3306bf8a9bd902 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Wed, 23 Sep 2015 03:25:29 -0400 Subject: [PATCH 10/19] Updated custom Exception javadocs. --- src/main/java/io/jsonwebtoken/IncorrectClaimException.java | 4 ++-- src/main/java/io/jsonwebtoken/InvalidClaimException.java | 6 +++++- src/main/java/io/jsonwebtoken/MissingClaimException.java | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/IncorrectClaimException.java b/src/main/java/io/jsonwebtoken/IncorrectClaimException.java index bf63cd5a..860a4d84 100644 --- a/src/main/java/io/jsonwebtoken/IncorrectClaimException.java +++ b/src/main/java/io/jsonwebtoken/IncorrectClaimException.java @@ -16,8 +16,8 @@ package io.jsonwebtoken; /** - * IncorrectClaimException is a subclass of the {@link InvalidClaimException} that is thrown after it is found that an - * expected claim has a value that is not expected. + * Exception thrown when discovering that a required claim does not equal the required value, indicating the JWT is + * invalid and may not be used. * * @since 0.6 */ diff --git a/src/main/java/io/jsonwebtoken/InvalidClaimException.java b/src/main/java/io/jsonwebtoken/InvalidClaimException.java index 9706a0b3..8880792c 100644 --- a/src/main/java/io/jsonwebtoken/InvalidClaimException.java +++ b/src/main/java/io/jsonwebtoken/InvalidClaimException.java @@ -16,7 +16,11 @@ package io.jsonwebtoken; /** - * InvalidClaimException is a subclass of the {@link ClaimJwtException} that is thrown after a validation of an JTW claim failed. + * Exception indicating a parsed claim is invalid in some way. Subclasses reflect the specific + * reason the claim is invalid. + * + * @see IncorrectClaimException + * @see MissingClaimException * * @since 0.6 */ diff --git a/src/main/java/io/jsonwebtoken/MissingClaimException.java b/src/main/java/io/jsonwebtoken/MissingClaimException.java index d453df57..030fe98d 100644 --- a/src/main/java/io/jsonwebtoken/MissingClaimException.java +++ b/src/main/java/io/jsonwebtoken/MissingClaimException.java @@ -16,8 +16,8 @@ package io.jsonwebtoken; /** - * MissingClaimException is a subclass of the {@link InvalidClaimException} that is thrown after it is found that an - * expected claim is missing. + * Exception thrown when discovering that a required claim is not present, indicating the JWT is + * invalid and may not be used. * * @since 0.6 */ From ddda2f92d3d5e36c1b5469eea553d89e7b6e6de0 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Wed, 23 Sep 2015 03:35:41 -0400 Subject: [PATCH 11/19] Renamed all the expect methods to require. --- src/main/java/io/jsonwebtoken/JwtParser.java | 12 +-- .../jsonwebtoken/impl/DefaultJwtParser.java | 12 +-- .../io/jsonwebtoken/JwtParserTest.groovy | 84 +++++++++---------- 3 files changed, 54 insertions(+), 54 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index 3e0cff47..1ed015ff 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -33,7 +33,7 @@ public interface JwtParser { * @param id * @return the parser for method chaining. */ - JwtParser expectId(String id); + JwtParser requireId(String id); /** * Sets an expected value for the subject claim. @@ -41,7 +41,7 @@ public interface JwtParser { * @param subject * @return the parser for method chaining. */ - JwtParser expectSubject(String subject); + JwtParser requireSubject(String subject); /** * Sets an expected value for the audience claim. @@ -49,7 +49,7 @@ public interface JwtParser { * @param audience * @return the parser for method chaining. */ - JwtParser expectAudience(String audience); + JwtParser requireAudience(String audience); /** * Sets an expected value for the issuer claim. @@ -57,7 +57,7 @@ public interface JwtParser { * @param issuer * @return the parser for method chaining. */ - JwtParser expectIssuer(String issuer); + JwtParser requireIssuer(String issuer); /** * Sets an expected value for the issuedAt claim. @@ -65,7 +65,7 @@ public interface JwtParser { * @param issuedAt * @return the parser for method chaining. */ - JwtParser expectIssuedAt(Date issuedAt); + JwtParser requireIssuedAt(Date issuedAt); /** * Sets an expected value for any given claim name. @@ -82,7 +82,7 @@ public interface JwtParser { * @param value * @return the parser for method chaining. */ - JwtParser expect(String claimName, Object value); + JwtParser require(String claimName, Object value); /** * Sets the signing key used to verify any discovered JWS digital signature. If the specified JWT string is not diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 5a815c76..a52e7b54 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -65,42 +65,42 @@ public class DefaultJwtParser implements JwtParser { Claims expectedClaims = new DefaultClaims(); @Override - public JwtParser expectIssuedAt(Date issuedAt) { + public JwtParser requireIssuedAt(Date issuedAt) { expectedClaims.setIssuedAt(issuedAt); return this; } @Override - public JwtParser expectIssuer(String issuer) { + public JwtParser requireIssuer(String issuer) { expectedClaims.setIssuer(issuer); return this; } @Override - public JwtParser expectAudience(String audience) { + public JwtParser requireAudience(String audience) { expectedClaims.setAudience(audience); return this; } @Override - public JwtParser expectSubject(String subject) { + public JwtParser requireSubject(String subject) { expectedClaims.setSubject(subject); return this; } @Override - public JwtParser expectId(String id) { + public JwtParser requireId(String id) { expectedClaims.setId(id); return this; } @Override - public JwtParser expect(String claimName, Object value) { + public JwtParser require(String claimName, Object value) { Assert.hasText(claimName, "claim name cannot be null or empty."); Assert.notNull(value, "The value cannot be null for claim name: " + claimName); expectedClaims.put(claimName, value); diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index ea0bda6a..4fbd36ca 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -727,7 +727,7 @@ class JwtParserTest { } @Test - void testParseExpectDontAllowNullClaimName() { + void testParseRequireDontAllowNullClaimName() { def expectedClaimValue = 'A Most Awesome Claim Value' byte[] key = randomKey() @@ -740,7 +740,7 @@ class JwtParserTest { try { // expecting null claim name, but with value Jwt jwt = Jwts.parser().setSigningKey(key). - expect(null, expectedClaimValue). + require(null, expectedClaimValue). parseClaimsJws(compact) fail() } catch (IllegalArgumentException e) { @@ -752,7 +752,7 @@ class JwtParserTest { } @Test - void testParseExpectDontAllowEmptyClaimName() { + void testParseRequireDontAllowEmptyClaimName() { def expectedClaimValue = 'A Most Awesome Claim Value' byte[] key = randomKey() @@ -765,7 +765,7 @@ class JwtParserTest { try { // expecting null claim name, but with value Jwt jwt = Jwts.parser().setSigningKey(key). - expect("", expectedClaimValue). + require("", expectedClaimValue). parseClaimsJws(compact) fail() } catch (IllegalArgumentException e) { @@ -777,7 +777,7 @@ class JwtParserTest { } @Test - void testParseExpectDontAllowNullClaimValue() { + void testParseRequireDontAllowNullClaimValue() { def expectedClaimName = 'A Most Awesome Claim Name' byte[] key = randomKey() @@ -790,7 +790,7 @@ class JwtParserTest { try { // expecting claim name, but with null value Jwt jwt = Jwts.parser().setSigningKey(key). - expect(expectedClaimName, null). + require(expectedClaimName, null). parseClaimsJws(compact) fail() } catch (IllegalArgumentException e) { @@ -802,7 +802,7 @@ class JwtParserTest { } @Test - void testParseExpectGeneric_Success() { + void testParseRequireGeneric_Success() { def expectedClaimName = 'A Most Awesome Claim Name' def expectedClaimValue = 'A Most Awesome Claim Value' @@ -813,14 +813,14 @@ class JwtParserTest { compact() Jwt jwt = Jwts.parser().setSigningKey(key). - expect(expectedClaimName, expectedClaimValue). + require(expectedClaimName, expectedClaimValue). parseClaimsJws(compact) assertEquals jwt.getBody().get(expectedClaimName), expectedClaimValue } @Test - void testParseExpectGeneric_Incorrect_Fail() { + void testParseRequireGeneric_Incorrect_Fail() { def goodClaimName = 'A Most Awesome Claim Name' def goodClaimValue = 'A Most Awesome Claim Value' @@ -834,7 +834,7 @@ class JwtParserTest { try { Jwts.parser().setSigningKey(key). - expect(goodClaimName, goodClaimValue). + require(goodClaimName, goodClaimValue). parseClaimsJws(compact) fail() } catch (IncorrectClaimException e) { @@ -846,7 +846,7 @@ class JwtParserTest { } @Test - void testParseExpectedGeneric_Missing_Fail() { + void testParseRequireedGeneric_Missing_Fail() { def claimName = 'A Most Awesome Claim Name' def claimValue = 'A Most Awesome Claim Value' @@ -858,7 +858,7 @@ class JwtParserTest { try { Jwt jwt = Jwts.parser().setSigningKey(key). - expect(claimName, claimValue). + require(claimName, claimValue). parseClaimsJws(compact) fail() } catch (MissingClaimException e) { @@ -870,7 +870,7 @@ class JwtParserTest { } @Test - void testParseExpectIssuedAt_Success() { + void testParseRequireIssuedAt_Success() { def issuedAt = new Date(System.currentTimeMillis()) byte[] key = randomKey() @@ -880,7 +880,7 @@ class JwtParserTest { compact() Jwt jwt = Jwts.parser().setSigningKey(key). - expectIssuedAt(issuedAt). + requireIssuedAt(issuedAt). parseClaimsJws(compact) // system converts to seconds (lopping off millis precision), then returns millis @@ -890,7 +890,7 @@ class JwtParserTest { } @Test - void testParseExpectIssuedAt_Incorrect_Fail() { + void testParseRequireIssuedAt_Incorrect_Fail() { def goodIssuedAt = new Date(System.currentTimeMillis()) def badIssuedAt = new Date(System.currentTimeMillis() - 10000) @@ -902,7 +902,7 @@ class JwtParserTest { try { Jwts.parser().setSigningKey(key). - expectIssuedAt(goodIssuedAt). + requireIssuedAt(goodIssuedAt). parseClaimsJws(compact) fail() } catch(IncorrectClaimException e) { @@ -914,7 +914,7 @@ class JwtParserTest { } @Test - void testParseExpectIssuedAt_Missing_Fail() { + void testParseRequireIssuedAt_Missing_Fail() { def issuedAt = new Date(System.currentTimeMillis() - 10000) byte[] key = randomKey() @@ -925,7 +925,7 @@ class JwtParserTest { try { Jwts.parser().setSigningKey(key). - expectIssuedAt(issuedAt). + requireIssuedAt(issuedAt). parseClaimsJws(compact) fail() } catch(MissingClaimException e) { @@ -937,7 +937,7 @@ class JwtParserTest { } @Test - void testParseExpectIssuer_Success() { + void testParseRequireIssuer_Success() { def issuer = 'A Most Awesome Issuer' byte[] key = randomKey() @@ -947,14 +947,14 @@ class JwtParserTest { compact() Jwt jwt = Jwts.parser().setSigningKey(key). - expectIssuer(issuer). + requireIssuer(issuer). parseClaimsJws(compact) assertEquals jwt.getBody().getIssuer(), issuer } @Test - void testParseExpectIssuer_Incorrect_Fail() { + void testParseRequireIssuer_Incorrect_Fail() { def goodIssuer = 'A Most Awesome Issuer' def badIssuer = 'A Most Bogus Issuer' @@ -966,7 +966,7 @@ class JwtParserTest { try { Jwts.parser().setSigningKey(key). - expectIssuer(goodIssuer). + requireIssuer(goodIssuer). parseClaimsJws(compact) fail() } catch(IncorrectClaimException e) { @@ -978,7 +978,7 @@ class JwtParserTest { } @Test - void testParseExpectIssuer_Missing_Fail() { + void testParseRequireIssuer_Missing_Fail() { def issuer = 'A Most Awesome Issuer' byte[] key = randomKey() @@ -989,7 +989,7 @@ class JwtParserTest { try { Jwts.parser().setSigningKey(key). - expectIssuer(issuer). + requireIssuer(issuer). parseClaimsJws(compact) fail() } catch(MissingClaimException e) { @@ -1001,7 +1001,7 @@ class JwtParserTest { } @Test - void testParseExpectAudience_Success() { + void testParseRequireAudience_Success() { def audience = 'A Most Awesome Audience' byte[] key = randomKey() @@ -1011,14 +1011,14 @@ class JwtParserTest { compact() Jwt jwt = Jwts.parser().setSigningKey(key). - expectAudience(audience). + requireAudience(audience). parseClaimsJws(compact) assertEquals jwt.getBody().getAudience(), audience } @Test - void testParseExpectAudience_Incorrect_Fail() { + void testParseRequireAudience_Incorrect_Fail() { def goodAudience = 'A Most Awesome Audience' def badAudience = 'A Most Bogus Audience' @@ -1030,7 +1030,7 @@ class JwtParserTest { try { Jwts.parser().setSigningKey(key). - expectAudience(goodAudience). + requireAudience(goodAudience). parseClaimsJws(compact) fail() } catch(IncorrectClaimException e) { @@ -1042,7 +1042,7 @@ class JwtParserTest { } @Test - void testParseExpectAudience_Missing_Fail() { + void testParseRequireAudience_Missing_Fail() { def audience = 'A Most Awesome audience' byte[] key = randomKey() @@ -1053,7 +1053,7 @@ class JwtParserTest { try { Jwts.parser().setSigningKey(key). - expectAudience(audience). + requireAudience(audience). parseClaimsJws(compact) fail() } catch(MissingClaimException e) { @@ -1065,7 +1065,7 @@ class JwtParserTest { } @Test - void testParseExpectSubject_Success() { + void testParseRequireSubject_Success() { def subject = 'A Most Awesome Subject' byte[] key = randomKey() @@ -1075,14 +1075,14 @@ class JwtParserTest { compact() Jwt jwt = Jwts.parser().setSigningKey(key). - expectSubject(subject). + requireSubject(subject). parseClaimsJws(compact) assertEquals jwt.getBody().getSubject(), subject } @Test - void testParseExpectSubject_Incorrect_Fail() { + void testParseRequireSubject_Incorrect_Fail() { def goodSubject = 'A Most Awesome Subject' def badSubject = 'A Most Bogus Subject' @@ -1094,7 +1094,7 @@ class JwtParserTest { try { Jwts.parser().setSigningKey(key). - expectSubject(goodSubject). + requireSubject(goodSubject). parseClaimsJws(compact) fail() } catch(IncorrectClaimException e) { @@ -1106,7 +1106,7 @@ class JwtParserTest { } @Test - void testParseExpectSubject_Missing_Fail() { + void testParseRequireSubject_Missing_Fail() { def subject = 'A Most Awesome Subject' byte[] key = randomKey() @@ -1117,7 +1117,7 @@ class JwtParserTest { try { Jwts.parser().setSigningKey(key). - expectSubject(subject). + requireSubject(subject). parseClaimsJws(compact) fail() } catch(MissingClaimException e) { @@ -1129,7 +1129,7 @@ class JwtParserTest { } @Test - void testParseExpectId_Success() { + void testParseRequireId_Success() { def id = 'A Most Awesome id' byte[] key = randomKey() @@ -1139,14 +1139,14 @@ class JwtParserTest { compact() Jwt jwt = Jwts.parser().setSigningKey(key). - expectId(id). + requireId(id). parseClaimsJws(compact) assertEquals jwt.getBody().getId(), id } @Test - void testParseExpectId_Incorrect_Fail() { + void testParseRequireId_Incorrect_Fail() { def goodId = 'A Most Awesome Id' def badId = 'A Most Bogus Id' @@ -1158,7 +1158,7 @@ class JwtParserTest { try { Jwts.parser().setSigningKey(key). - expectId(goodId). + requireId(goodId). parseClaimsJws(compact) fail() } catch(IncorrectClaimException e) { @@ -1170,7 +1170,7 @@ class JwtParserTest { } @Test - void testParseExpectId_Missing_Fail() { + void testParseRequireId_Missing_Fail() { def id = 'A Most Awesome Id' byte[] key = randomKey() @@ -1181,7 +1181,7 @@ class JwtParserTest { try { Jwts.parser().setSigningKey(key). - expectId(id). + requireId(id). parseClaimsJws(compact) fail() } catch(MissingClaimException e) { From 2e452a42b10173ee0708eedacb845ba0b315c5d0 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Wed, 23 Sep 2015 04:33:31 -0400 Subject: [PATCH 12/19] Added requireExpiration and requireNotBefore --- src/main/java/io/jsonwebtoken/JwtParser.java | 16 +++ .../jsonwebtoken/impl/DefaultJwtParser.java | 14 ++ .../io/jsonwebtoken/JwtParserTest.groovy | 136 ++++++++++++++++++ 3 files changed, 166 insertions(+) diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index 1ed015ff..828563ea 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -67,6 +67,22 @@ public interface JwtParser { */ JwtParser requireIssuedAt(Date issuedAt); + /** + * Sets an expected value for the expiration claim. + * + * @param expiration + * @return the parser for method chaining. + */ + JwtParser requireExpiration(Date expiration); + + /** + * Sets an expected value for the notBefore claim. + * + * @param notBefore + * @return the parser for method chaining + */ + JwtParser requireNotBefore(Date notBefore); + /** * Sets an expected value for any given claim name. * diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index a52e7b54..37c5f5a0 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -99,6 +99,20 @@ public class DefaultJwtParser implements JwtParser { return this; } + @Override + public JwtParser requireExpiration(Date expiration) { + expectedClaims.setExpiration(expiration); + + return this; + } + + @Override + public JwtParser requireNotBefore(Date notBefore) { + expectedClaims.setNotBefore(notBefore); + + return this; + } + @Override public JwtParser require(String claimName, Object value) { Assert.hasText(claimName, "claim name cannot be null or empty."); diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 4fbd36ca..a65aa4dc 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -1191,4 +1191,140 @@ class JwtParserTest { ) } } + + @Test + void testParseRequireExpiration_Success() { + // expire in the future + def expiration = new Date(System.currentTimeMillis() + 10000) + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setExpiration(expiration). + compact() + + Jwt jwt = Jwts.parser().setSigningKey(key). + requireExpiration(expiration). + parseClaimsJws(compact) + + // system converts to seconds (lopping off millis precision), then returns millis + def expirationMillis = ((long)expiration.getTime() / 1000) * 1000 + + assertEquals jwt.getBody().getExpiration().getTime(), expirationMillis + } + + @Test + void testParseRequireExpirationAt_Incorrect_Fail() { + def goodExpiration = new Date(System.currentTimeMillis() + 20000) + def badExpiration = new Date(System.currentTimeMillis() + 10000) + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setExpiration(badExpiration). + compact() + + try { + Jwts.parser().setSigningKey(key). + requireExpiration(goodExpiration). + parseClaimsJws(compact) + fail() + } catch(IncorrectClaimException e) { + assertEquals( + String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.EXPIRATION, goodExpiration, badExpiration), + e.getMessage() + ) + } + } + + @Test + void testParseRequireExpiration_Missing_Fail() { + def expiration = new Date(System.currentTimeMillis() + 10000) + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setSubject("Dummy"). + compact() + + try { + Jwts.parser().setSigningKey(key). + requireExpiration(expiration). + parseClaimsJws(compact) + fail() + } catch(MissingClaimException e) { + assertEquals( + String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.EXPIRATION, expiration), + e.getMessage() + ) + } + } + + @Test + void testParseRequireNotBefore_Success() { + // expire in the future + def notBefore = new Date(System.currentTimeMillis() - 10000) + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setNotBefore(notBefore). + compact() + + Jwt jwt = Jwts.parser().setSigningKey(key). + requireNotBefore(notBefore). + parseClaimsJws(compact) + + // system converts to seconds (lopping off millis precision), then returns millis + def notBeforeMillis = ((long)notBefore.getTime() / 1000) * 1000 + + assertEquals jwt.getBody().getNotBefore().getTime(), notBeforeMillis + } + + @Test + void testParseRequireNotBefore_Incorrect_Fail() { + def goodNotBefore = new Date(System.currentTimeMillis() - 20000) + def badNotBefore = new Date(System.currentTimeMillis() - 10000) + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setNotBefore(badNotBefore). + compact() + + try { + Jwts.parser().setSigningKey(key). + requireNotBefore(goodNotBefore). + parseClaimsJws(compact) + fail() + } catch(IncorrectClaimException e) { + assertEquals( + String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.NOT_BEFORE, goodNotBefore, badNotBefore), + e.getMessage() + ) + } + } + + @Test + void testParseRequireNotBefore_Missing_Fail() { + def notBefore = new Date(System.currentTimeMillis() - 10000) + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setSubject("Dummy"). + compact() + + try { + Jwts.parser().setSigningKey(key). + requireNotBefore(notBefore). + parseClaimsJws(compact) + fail() + } catch(MissingClaimException e) { + assertEquals( + String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.NOT_BEFORE, notBefore), + e.getMessage() + ) + } + } } From b4015be11e4337977f743fe0d78d949c14ec3ae8 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Wed, 23 Sep 2015 03:37:47 -0400 Subject: [PATCH 13/19] Added in test that does NOT work for custom claim with Date type. --- .../groovy/io/jsonwebtoken/JwtParserTest.groovy | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index a65aa4dc..60299845 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -1327,4 +1327,21 @@ class JwtParserTest { ) } } + +// @Test +// void testParseExpectedCustomDate_Success() { +// def aDate = new Date(System.currentTimeMillis()) +// +// byte[] key = randomKey() +// +// String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). +// claim("aDate", aDate). +// compact() +// +// Jwt jwt = Jwts.parser().setSigningKey(key). +// expect("aDate", aDate). +// parseClaimsJws(compact) +// +// assertEquals jwt.getBody().get("aDate"), aDate +// } } From 5d320d22a55c4822195852d7fded90101a728eca Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Wed, 23 Sep 2015 15:43:15 -0400 Subject: [PATCH 14/19] Handled generic require for Date. Added ability to specify required type on get method of claim --- src/main/java/io/jsonwebtoken/Claims.java | 1 + .../jsonwebtoken/RequiredTypeException.java | 32 ++++++++ .../io/jsonwebtoken/impl/DefaultClaims.java | 17 ++++ .../jsonwebtoken/impl/DefaultJwtParser.java | 15 +--- .../io/jsonwebtoken/JwtParserTest.groovy | 80 +++++++++++++++---- .../RequiredTypeExceptionTest.groovy | 20 +++++ .../impl/DefaultClaimsTest.groovy | 78 ++++++++++++++++++ 7 files changed, 214 insertions(+), 29 deletions(-) create mode 100644 src/main/java/io/jsonwebtoken/RequiredTypeException.java create mode 100644 src/test/groovy/io/jsonwebtoken/RequiredTypeExceptionTest.groovy create mode 100644 src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy diff --git a/src/main/java/io/jsonwebtoken/Claims.java b/src/main/java/io/jsonwebtoken/Claims.java index d510aca8..22a3e364 100644 --- a/src/main/java/io/jsonwebtoken/Claims.java +++ b/src/main/java/io/jsonwebtoken/Claims.java @@ -170,4 +170,5 @@ public interface Claims extends Map, ClaimsMutator { @Override //only for better/targeted JavaDoc Claims setId(String jti); + T get(String claimName, Class requiredType); } diff --git a/src/main/java/io/jsonwebtoken/RequiredTypeException.java b/src/main/java/io/jsonwebtoken/RequiredTypeException.java new file mode 100644 index 00000000..eeb60d30 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/RequiredTypeException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2014 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken; + +/** + * Exception thrown when {@link Claims#get(String, Class)} is called and the value does not match the type of the + * {@code Class} argument. + * + * @since 0.6 + */ +public class RequiredTypeException extends JwtException { + public RequiredTypeException(String message) { + super(message); + } + + public RequiredTypeException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java b/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java index 5e6284eb..38df87ee 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java @@ -16,6 +16,7 @@ package io.jsonwebtoken.impl; import io.jsonwebtoken.Claims; +import io.jsonwebtoken.RequiredTypeException; import java.util.Date; import java.util.Map; @@ -106,4 +107,20 @@ public class DefaultClaims extends JwtMap implements Claims { setValue(Claims.ID, jti); return this; } + + @Override + public T get(String claimName, Class requiredType) { + Object value = get(claimName); + if (value == null) { return null; } + + if (requiredType == Date.class && value instanceof Long) { + value = new Date((Long)value); + } + + if (!requiredType.isInstance(value)) { + throw new RequiredTypeException("Expected value to be of type: " + requiredType + ", but was " + value.getClass()); + } + + return requiredType.cast(value); + } } diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 37c5f5a0..9bbe3b2b 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -378,31 +378,20 @@ public class DefaultJwtParser implements JwtParser { private void validateExpectedClaims(Header header, Claims claims) { for (String expectedClaimName : expectedClaims.keySet()) { - // this will be overridden if one of the default claims is used Object expectedClaimValue = expectedClaims.get(expectedClaimName); Object actualClaimValue = claims.get(expectedClaimName); if (Claims.ISSUED_AT.equals(expectedClaimName)) { expectedClaimValue = expectedClaims.getIssuedAt(); actualClaimValue = claims.getIssuedAt(); - } else if (Claims.AUDIENCE.equals(expectedClaimName)) { - expectedClaimValue = expectedClaims.getAudience(); - actualClaimValue = claims.getAudience(); - } else if (Claims.ISSUER.equals(expectedClaimName)) { - expectedClaimValue = expectedClaims.getIssuer(); - actualClaimValue = claims.getIssuer(); - } else if (Claims.SUBJECT.equals(expectedClaimName)) { - expectedClaimValue = expectedClaims.getSubject(); - actualClaimValue = claims.getSubject(); } else if (Claims.EXPIRATION.equals(expectedClaimName)) { expectedClaimValue = expectedClaims.getExpiration(); actualClaimValue = claims.getExpiration(); } else if (Claims.NOT_BEFORE.equals(expectedClaimName)) { expectedClaimValue = expectedClaims.getNotBefore(); actualClaimValue = claims.getNotBefore(); - } else if (Claims.ID.equals(expectedClaimName)) { - expectedClaimValue = expectedClaims.getId(); - actualClaimValue = claims.getId(); + } else if (expectedClaimValue instanceof Date && actualClaimValue != null && actualClaimValue instanceof Long) { + actualClaimValue = new Date((Long)actualClaimValue); } InvalidClaimException invalidClaimException = null; diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 60299845..3e37658f 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -1328,20 +1328,68 @@ class JwtParserTest { } } -// @Test -// void testParseExpectedCustomDate_Success() { -// def aDate = new Date(System.currentTimeMillis()) -// -// byte[] key = randomKey() -// -// String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). -// claim("aDate", aDate). -// compact() -// -// Jwt jwt = Jwts.parser().setSigningKey(key). -// expect("aDate", aDate). -// parseClaimsJws(compact) -// -// assertEquals jwt.getBody().get("aDate"), aDate -// } + @Test + void testParseRequireCustomDate_Success() { + def aDate = new Date(System.currentTimeMillis()) + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + claim("aDate", aDate). + compact() + + Jwt jwt = Jwts.parser().setSigningKey(key). + require("aDate", aDate). + parseClaimsJws(compact) + + assertEquals jwt.getBody().get("aDate", Date.class), aDate + } + + @Test + void testParseRequireCustomDate_Incorrect_Fail() { + def goodDate = new Date(System.currentTimeMillis()) + def badDate = new Date(System.currentTimeMillis() - 10000) + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + claim("aDate", badDate). + compact() + + try { + Jwts.parser().setSigningKey(key). + require("aDate", goodDate). + parseClaimsJws(compact) + fail() + } catch(IncorrectClaimException e) { + assertEquals( + String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, "aDate", goodDate, badDate), + e.getMessage() + ) + } + + } + + @Test + void testParseRequireCustomDate_Missing_Fail() { + def aDate = new Date(System.currentTimeMillis()) + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setSubject("Dummy"). + compact() + + try { + Jwts.parser().setSigningKey(key). + require("aDate", aDate). + parseClaimsJws(compact) + fail() + } catch(MissingClaimException e) { + assertEquals( + String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, "aDate", aDate), + e.getMessage() + ) + } + } } diff --git a/src/test/groovy/io/jsonwebtoken/RequiredTypeExceptionTest.groovy b/src/test/groovy/io/jsonwebtoken/RequiredTypeExceptionTest.groovy new file mode 100644 index 00000000..dded945d --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/RequiredTypeExceptionTest.groovy @@ -0,0 +1,20 @@ +package io.jsonwebtoken + +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertSame + +class RequiredTypeExceptionTest { + @Test + void testOverloadedConstructor() { + def msg = 'foo' + def cause = new NullPointerException() + + def ex = new RequiredTypeException(msg, cause) + + assertEquals ex.message, msg + assertSame ex.cause, cause + } + +} diff --git a/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy new file mode 100644 index 00000000..cb584a4d --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2015 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl + +import io.jsonwebtoken.Claims +import io.jsonwebtoken.RequiredTypeException +import org.junit.Before +import org.junit.Test +import static org.junit.Assert.* + +class DefaultClaimsTest { + + Claims claims + + @Before + void setup() { + claims = new DefaultClaims() + } + + @Test + void testGetClaimWithRequiredType_Null_Success() { + claims.put("aNull", null) + Object result = claims.get("aNull", Integer.class) + assertNull(result) + } + + @Test + void testGetClaimWithRequiredType_Exception() { + claims.put("anInteger", new Integer(5)) + try { + claims.get("anInteger", String.class) + fail() + } catch (RequiredTypeException e) { + assertEquals( + "Expected value to be of type: class java.lang.String, but was class java.lang.Integer", + e.getMessage() + ) + } + } + + @Test + void testGetClaimWithRequiredType_Success() { + claims.put("anInteger", new Integer(5)) + Object result = claims.get("anInteger", Integer.class) + + assertTrue(result instanceof Integer) + } + + @Test + void testGetClaimWithRequiredType_Date_Success() { + def actual = new Date(); + claims.put("aDate", actual) + Date expected = claims.get("aDate", Date.class); + assertEquals(expected, actual); + } + + @Test + void testGetClaimWithRequiredType_DateWithLong_Success() { + def actual = new Date(); + // note that Long is stored in claim + claims.put("aDate", actual.getTime()) + Date expected = claims.get("aDate", Date.class); + assertEquals(expected, actual); + } +} From 6401727b2a0c263b81c4d465cdace778efc137b5 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Wed, 23 Sep 2015 17:03:07 -0400 Subject: [PATCH 15/19] Refactored generic get with required type to handle the official claim types that are dates. Updated javadocs to reflect 'require' language. --- src/main/java/io/jsonwebtoken/JwtParser.java | 34 +++++++++++-------- .../io/jsonwebtoken/impl/DefaultClaims.java | 13 +++++-- .../jsonwebtoken/impl/DefaultJwtParser.java | 22 ++++++------ .../impl/DefaultClaimsTest.groovy | 29 ++++++++++++++-- 4 files changed, 69 insertions(+), 29 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index 828563ea..44019f77 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -28,15 +28,17 @@ public interface JwtParser { public static final char SEPARATOR_CHAR = '.'; /** - * Sets an expected value for the jti claim. + * Ensures that the specified {@code jti} value is present when parsing the JWT. If not present, + * an exception will be thrown indicating that the JWT is invalid and may not be used. * * @param id - * @return the parser for method chaining. + * @return the parser method for chaining. */ JwtParser requireId(String id); /** - * Sets an expected value for the subject claim. + * Ensures that the specified {@code sub} value is present when parsing the JWT. If not present, + * an exception will be thrown indicating that the JWT is invalid and may not be used. * * @param subject * @return the parser for method chaining. @@ -44,7 +46,8 @@ public interface JwtParser { JwtParser requireSubject(String subject); /** - * Sets an expected value for the audience claim. + * Ensures that the specified {@code aud} value is present when parsing the JWT. If not present, + * an exception will be thrown indicating that the JWT is invalid and may not be used. * * @param audience * @return the parser for method chaining. @@ -52,7 +55,8 @@ public interface JwtParser { JwtParser requireAudience(String audience); /** - * Sets an expected value for the issuer claim. + * Ensures that the specified {@code iss} value is present when parsing the JWT. If not present, + * an exception will be thrown indicating that the JWT is invalid and may not be used. * * @param issuer * @return the parser for method chaining. @@ -60,7 +64,8 @@ public interface JwtParser { JwtParser requireIssuer(String issuer); /** - * Sets an expected value for the issuedAt claim. + * Ensures that the specified {@code iat} value is present when parsing the JWT. If not present, + * an exception will be thrown indicating that the JWT is invalid and may not be used. * * @param issuedAt * @return the parser for method chaining. @@ -68,7 +73,8 @@ public interface JwtParser { JwtParser requireIssuedAt(Date issuedAt); /** - * Sets an expected value for the expiration claim. + * Ensures that the specified {@code exp} value is present when parsing the JWT. If not present, + * an exception will be thrown indicating that the JWT is invalid and may not be used. * * @param expiration * @return the parser for method chaining. @@ -76,7 +82,8 @@ public interface JwtParser { JwtParser requireExpiration(Date expiration); /** - * Sets an expected value for the notBefore claim. + * Ensures that the specified {@code nbf} value is present when parsing the JWT. If not present, + * an exception will be thrown indicating that the JWT is invalid and may not be used. * * @param notBefore * @return the parser for method chaining @@ -84,15 +91,14 @@ public interface JwtParser { JwtParser requireNotBefore(Date notBefore); /** - * Sets an expected value for any given claim name. + * Ensures that the specified {@code claimName} value is present when parsing the JWT. If not present, + * an exception will be thrown indicating that the JWT is invalid and may not be used. * - * If an expectation is set for a particular claim name and the JWT being parsed does not have that claim set, + * If a particular claim is required and the JWT being parsed does not have that claim set, * a {@Link MissingClaimException} will be thrown. * - * If an expectation is set for a particular claim name and the JWT being parsed has a value that is different than - * the expected value, a {@link IncorrectClaimException} will be thrown. - * - * If either {@code claimName} is null or empty or {@code value} is null, the expectation is simply ignored. + * If a particular claim is required and the JWT being parsed has a value that is different than + * the required value, a {@link IncorrectClaimException} will be thrown. * * @param claimName * @param value diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java b/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java index 38df87ee..196c82ff 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java @@ -66,7 +66,7 @@ public class DefaultClaims extends JwtMap implements Claims { @Override public Date getExpiration() { - return getDate(Claims.EXPIRATION); + return get(Claims.EXPIRATION, Date.class); } @Override @@ -77,7 +77,7 @@ public class DefaultClaims extends JwtMap implements Claims { @Override public Date getNotBefore() { - return getDate(Claims.NOT_BEFORE); + return get(Claims.NOT_BEFORE, Date.class); } @Override @@ -88,7 +88,7 @@ public class DefaultClaims extends JwtMap implements Claims { @Override public Date getIssuedAt() { - return getDate(Claims.ISSUED_AT); + return get(Claims.ISSUED_AT, Date.class); } @Override @@ -113,6 +113,13 @@ public class DefaultClaims extends JwtMap implements Claims { Object value = get(claimName); if (value == null) { return null; } + if (Claims.EXPIRATION.equals(claimName) || + Claims.ISSUED_AT.equals(claimName) || + Claims.NOT_BEFORE.equals(claimName) + ) { + value = getDate(claimName); + } + if (requiredType == Date.class && value instanceof Long) { value = new Date((Long)value); } diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 9bbe3b2b..a759549d 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -381,16 +381,18 @@ public class DefaultJwtParser implements JwtParser { Object expectedClaimValue = expectedClaims.get(expectedClaimName); Object actualClaimValue = claims.get(expectedClaimName); - if (Claims.ISSUED_AT.equals(expectedClaimName)) { - expectedClaimValue = expectedClaims.getIssuedAt(); - actualClaimValue = claims.getIssuedAt(); - } else if (Claims.EXPIRATION.equals(expectedClaimName)) { - expectedClaimValue = expectedClaims.getExpiration(); - actualClaimValue = claims.getExpiration(); - } else if (Claims.NOT_BEFORE.equals(expectedClaimName)) { - expectedClaimValue = expectedClaims.getNotBefore(); - actualClaimValue = claims.getNotBefore(); - } else if (expectedClaimValue instanceof Date && actualClaimValue != null && actualClaimValue instanceof Long) { + if ( + Claims.ISSUED_AT.equals(expectedClaimName) || + Claims.EXPIRATION.equals(expectedClaimName) || + Claims.NOT_BEFORE.equals(expectedClaimName) + ) { + expectedClaimValue = expectedClaims.get(expectedClaimName, Date.class); + actualClaimValue = claims.get(expectedClaimName, Date.class); + } else if ( + expectedClaimValue instanceof Date && + actualClaimValue != null && + actualClaimValue instanceof Long + ) { actualClaimValue = new Date((Long)actualClaimValue); } diff --git a/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy index cb584a4d..161957f0 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy @@ -64,7 +64,7 @@ class DefaultClaimsTest { def actual = new Date(); claims.put("aDate", actual) Date expected = claims.get("aDate", Date.class); - assertEquals(expected, actual); + assertEquals(expected, actual) } @Test @@ -73,6 +73,31 @@ class DefaultClaimsTest { // note that Long is stored in claim claims.put("aDate", actual.getTime()) Date expected = claims.get("aDate", Date.class); - assertEquals(expected, actual); + assertEquals(expected, actual) } + + @Test + void testGetClaimExpiration_Success() { + def now = new Date(System.currentTimeMillis()) + claims.setExpiration(now) + Date expected = claims.get("exp", Date.class) + assertEquals(expected, claims.getExpiration()) + } + + @Test + void testGetClaimIssuedAt_Success() { + def now = new Date(System.currentTimeMillis()) + claims.setIssuedAt(now) + Date expected = claims.get("iat", Date.class) + assertEquals(expected, claims.getIssuedAt()) + } + + @Test + void testGetClaimNotBefore_Success() { + def now = new Date(System.currentTimeMillis()) + claims.setNotBefore(now) + Date expected = claims.get("nbf", Date.class) + assertEquals(expected, claims.getNotBefore()) + } + } From 681a3fc0bad92e61a15f4df36c7d10b7ebcc4dd7 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Wed, 23 Sep 2015 17:20:02 -0400 Subject: [PATCH 16/19] Added coverage test for JwtMap. --- src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy index 1b155b3e..0d00f6d7 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy @@ -20,6 +20,12 @@ import static org.junit.Assert.* class JwtMapTest { + @Test + void testToDateFromNull() { + Date actual = JwtMap.toDate(null, 'foo') + assertNull actual + } + @Test void testToDateFromDate() { From a22a76ad79d202798a2f36518cb7a7288df56ff2 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Wed, 23 Sep 2015 17:40:50 -0400 Subject: [PATCH 17/19] Update to javadocs to make more clear. --- src/main/java/io/jsonwebtoken/JwtParser.java | 62 +++++++++++++------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index 44019f77..dd9b1271 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -28,81 +28,99 @@ public interface JwtParser { public static final char SEPARATOR_CHAR = '.'; /** - * Ensures that the specified {@code jti} value is present when parsing the JWT. If not present, - * an exception will be thrown indicating that the JWT is invalid and may not be used. + * Ensures that the specified {@code jti} exists in the parsed JWT. If missing or if the parsed + * value does not equal the specified value, an exception will be thrown indicating that the + * JWT is invalid and may not be used. * * @param id * @return the parser method for chaining. + * @see MissingClaimException + * @see IncorrectClaimException */ JwtParser requireId(String id); /** - * Ensures that the specified {@code sub} value is present when parsing the JWT. If not present, - * an exception will be thrown indicating that the JWT is invalid and may not be used. + * Ensures that the specified {@code sub} exists in the parsed JWT. If missing or if the parsed + * value does not equal the specified value, an exception will be thrown indicating that the + * JWT is invalid and may not be used. * * @param subject * @return the parser for method chaining. + * @see MissingClaimException + * @see IncorrectClaimException */ JwtParser requireSubject(String subject); /** - * Ensures that the specified {@code aud} value is present when parsing the JWT. If not present, - * an exception will be thrown indicating that the JWT is invalid and may not be used. + * Ensures that the specified {@code aud} exists in the parsed JWT. If missing or if the parsed + * value does not equal the specified value, an exception will be thrown indicating that the + * JWT is invalid and may not be used. * * @param audience * @return the parser for method chaining. + * @see MissingClaimException + * @see IncorrectClaimException */ JwtParser requireAudience(String audience); /** - * Ensures that the specified {@code iss} value is present when parsing the JWT. If not present, - * an exception will be thrown indicating that the JWT is invalid and may not be used. + * Ensures that the specified {@code iss} exists in the parsed JWT. If missing or if the parsed + * value does not equal the specified value, an exception will be thrown indicating that the + * JWT is invalid and may not be used. * * @param issuer * @return the parser for method chaining. + * @see MissingClaimException + * @see IncorrectClaimException */ JwtParser requireIssuer(String issuer); /** - * Ensures that the specified {@code iat} value is present when parsing the JWT. If not present, - * an exception will be thrown indicating that the JWT is invalid and may not be used. + * Ensures that the specified {@code iat} exists in the parsed JWT. If missing or if the parsed + * value does not equal the specified value, an exception will be thrown indicating that the + * JWT is invalid and may not be used. * * @param issuedAt * @return the parser for method chaining. + * @see MissingClaimException + * @see IncorrectClaimException */ JwtParser requireIssuedAt(Date issuedAt); /** - * Ensures that the specified {@code exp} value is present when parsing the JWT. If not present, - * an exception will be thrown indicating that the JWT is invalid and may not be used. + * Ensures that the specified {@code exp} exists in the parsed JWT. If missing or if the parsed + * value does not equal the specified value, an exception will be thrown indicating that the + * JWT is invalid and may not be used. * * @param expiration * @return the parser for method chaining. + * @see MissingClaimException + * @see IncorrectClaimException */ JwtParser requireExpiration(Date expiration); /** - * Ensures that the specified {@code nbf} value is present when parsing the JWT. If not present, - * an exception will be thrown indicating that the JWT is invalid and may not be used. + * Ensures that the specified {@code nbf} exists in the parsed JWT. If missing or if the parsed + * value does not equal the specified value, an exception will be thrown indicating that the + * JWT is invalid and may not be used. * * @param notBefore * @return the parser for method chaining + * @see MissingClaimException + * @see IncorrectClaimException */ JwtParser requireNotBefore(Date notBefore); /** - * Ensures that the specified {@code claimName} value is present when parsing the JWT. If not present, - * an exception will be thrown indicating that the JWT is invalid and may not be used. - * - * If a particular claim is required and the JWT being parsed does not have that claim set, - * a {@Link MissingClaimException} will be thrown. - * - * If a particular claim is required and the JWT being parsed has a value that is different than - * the required value, a {@link IncorrectClaimException} will be thrown. + * Ensures that the specified {@code claimName} exists in the parsed JWT. If missing or if the parsed + * value does not equal the specified value, an exception will be thrown indicating that the + * JWT is invalid and may not be used. * * @param claimName * @param value * @return the parser for method chaining. + * @see MissingClaimException + * @see IncorrectClaimException */ JwtParser require(String claimName, Object value); From 137b6db31d1876b967238491ff04d744ea96b2ac Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Wed, 23 Sep 2015 15:05:30 -0700 Subject: [PATCH 18/19] #42: Added readme/release documentation for required JWT field value functionality. --- README.md | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7ccdb2b3..ccac0a77 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,43 @@ These feature sets will be implemented in a future release when possible. Commu ### 0.6 -- Added the ability to set expectations when parsing a JWT which enforces a particular claim having a particular value +#### Enforce JWT Values when Parsing + +We added the ability to set expectations when parsing a JWT which ensures particular claims having particular values. + +For example, let's say that you require that the JWT you are parsing has a specific `sub` (subject) value, +otherwise you may not trust the token. You can do that by using one of the `require` methods on the parser builder: + +```java +try { + Jwts.parser().requireSubject("jsmith").setSigningKey(key).parseClaimsJws(s); +} catch(InvalidClaimException ice) { + // the sub field was missing or did not have a 'jsmith' value +} +``` + +If it is important to react to a missing vs an incorrect value, instead of catching `InvalidClaimException`, you can catch either `MissingClaimException` or `IncorrectClaimException`: + +```java +try { + Jwts.parser().requireSubject("jsmith").setSigningKey(key).parseClaimsJws(s); +} catch(MissingClaimException mce) { + // the parsed JWT did not have the sub field +} catch(IncorrectClaimException ice) { + // the parsed JWT had a sub field, but its value was not equal to 'jsmith' +} +``` + +You can also require custom fields by using the `require(name, requiredValue)` method - for example: + +```java +try { + Jwts.parser().require("myfield", "myRequiredValue").setSigningKey(key).parseClaimsJws(s); +} catch(InvalidClaimException ice) { + // the 'myfield' field was missing or did not have a 'myRequiredValue' value +} +``` +(or, again, you could catch either MissingClaimException or IncorrectClaimException instead) ### 0.5.1 From 3e80cd647b59effb91aaf1b14e082345466b05a0 Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Wed, 23 Sep 2015 15:07:46 -0700 Subject: [PATCH 19/19] Minor readability update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ccac0a77..655a333f 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ try { } ``` -You can also require custom fields by using the `require(name, requiredValue)` method - for example: +You can also require custom fields by using the `require(fieldName, requiredFieldValue)` method - for example: ```java try {