diff --git a/README.md b/README.md index f0a863a8..655a333f 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,46 @@ These feature sets will be implemented in a future release when possible. Commu ## Release Notes +### 0.6 + +#### 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(fieldName, requiredFieldValue)` 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 - 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/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/IncorrectClaimException.java b/src/main/java/io/jsonwebtoken/IncorrectClaimException.java new file mode 100644 index 00000000..860a4d84 --- /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; + +/** + * 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 + */ +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..8880792c --- /dev/null +++ b/src/main/java/io/jsonwebtoken/InvalidClaimException.java @@ -0,0 +1,54 @@ +/* + * 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; + +/** + * 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 + */ +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 909e73d0..c5da9764 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,103 @@ public interface JwtParser { public static final char SEPARATOR_CHAR = '.'; + /** + * 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} 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} 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} 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} 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} 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} 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} 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); + /** * 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..030fe98d --- /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; + +/** + * Exception thrown when discovering that a required claim is not present, indicating the JWT is + * invalid and may not be used. + * + * @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/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..196c82ff 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; @@ -65,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 @@ -76,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 @@ -87,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 @@ -106,4 +107,27 @@ 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 (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); + } + + 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 a3b372b2..df5d654f 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -16,13 +16,18 @@ package io.jsonwebtoken.impl; import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.ClaimJwtException; import io.jsonwebtoken.Claims; import io.jsonwebtoken.CompressionCodec; import io.jsonwebtoken.CompressionCodecResolver; 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; import io.jsonwebtoken.JwtHandlerAdapter; @@ -63,6 +68,66 @@ public class DefaultJwtParser implements JwtParser { private CompressionCodecResolver compressionCodecResolver = new DefaultCompressionCodecResolver(); + Claims expectedClaims = new DefaultClaims(); + + @Override + public JwtParser requireIssuedAt(Date issuedAt) { + expectedClaims.setIssuedAt(issuedAt); + + return this; + } + + @Override + public JwtParser requireIssuer(String issuer) { + expectedClaims.setIssuer(issuer); + + return this; + } + + @Override + public JwtParser requireAudience(String audience) { + expectedClaims.setAudience(audience); + + return this; + } + + @Override + public JwtParser requireSubject(String subject) { + expectedClaims.setSubject(subject); + + return this; + } + + @Override + public JwtParser requireId(String id) { + expectedClaims.setId(id); + + 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."); + Assert.notNull(value, "The value cannot be null for claim name: " + claimName); + expectedClaims.put(claimName, value); + + return this; + } + @Override public JwtParser setSigningKey(byte[] key) { Assert.notEmpty(key, "signing key cannot be null or empty."); @@ -320,6 +385,8 @@ public class DefaultJwtParser implements JwtParser { throw new PrematureJwtException(header, claims, msg); } } + + validateExpectedClaims(header, claims); } Object body = claims != null ? claims : payload; @@ -331,6 +398,51 @@ 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); + + 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); + } + + 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..3e37658f 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 { @@ -723,4 +725,671 @@ class JwtParserTest { 'method or, for HMAC algorithms, the resolveSigningKeyBytes(JwsHeader, String) method.' } } + + @Test + void testParseRequireDontAllowNullClaimName() { + 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() + + try { + // expecting null claim name, but with value + Jwt jwt = Jwts.parser().setSigningKey(key). + require(null, expectedClaimValue). + parseClaimsJws(compact) + fail() + } catch (IllegalArgumentException e) { + assertEquals( + "claim name cannot be null or empty.", + e.getMessage() + ) + } + } + + @Test + void testParseRequireDontAllowEmptyClaimName() { + 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() + + try { + // expecting null claim name, but with value + Jwt jwt = Jwts.parser().setSigningKey(key). + require("", expectedClaimValue). + parseClaimsJws(compact) + fail() + } catch (IllegalArgumentException e) { + assertEquals( + "claim name cannot be null or empty.", + e.getMessage() + ) + } + } + + @Test + void testParseRequireDontAllowNullClaimValue() { + 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() + + try { + // expecting claim name, but with null value + Jwt jwt = Jwts.parser().setSigningKey(key). + require(expectedClaimName, null). + parseClaimsJws(compact) + fail() + } catch (IllegalArgumentException e) { + assertEquals( + "The value cannot be null for claim name: " + expectedClaimName, + e.getMessage() + ) + } + } + + @Test + void testParseRequireGeneric_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). + require(expectedClaimName, expectedClaimValue). + parseClaimsJws(compact) + + assertEquals jwt.getBody().get(expectedClaimName), expectedClaimValue + } + + @Test + void testParseRequireGeneric_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). + require(goodClaimName, goodClaimValue). + parseClaimsJws(compact) + fail() + } catch (IncorrectClaimException e) { + assertEquals( + String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, goodClaimName, goodClaimValue, badClaimValue), + e.getMessage() + ) + } + } + + @Test + void testParseRequireedGeneric_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). + require(claimName, claimValue). + parseClaimsJws(compact) + fail() + } catch (MissingClaimException e) { + assertEquals( + String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, claimName, claimValue), + e.getMessage() + ) + } + } + + @Test + void testParseRequireIssuedAt_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). + requireIssuedAt(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 testParseRequireIssuedAt_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). + requireIssuedAt(goodIssuedAt). + parseClaimsJws(compact) + fail() + } catch(IncorrectClaimException e) { + assertEquals( + String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.ISSUED_AT, goodIssuedAt, badIssuedAt), + e.getMessage() + ) + } + } + + @Test + void testParseRequireIssuedAt_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). + requireIssuedAt(issuedAt). + parseClaimsJws(compact) + fail() + } catch(MissingClaimException e) { + assertEquals( + String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.ISSUED_AT, issuedAt), + e.getMessage() + ) + } + } + + @Test + void testParseRequireIssuer_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). + requireIssuer(issuer). + parseClaimsJws(compact) + + assertEquals jwt.getBody().getIssuer(), issuer + } + + @Test + void testParseRequireIssuer_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). + requireIssuer(goodIssuer). + parseClaimsJws(compact) + fail() + } catch(IncorrectClaimException e) { + assertEquals( + String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.ISSUER, goodIssuer, badIssuer), + e.getMessage() + ) + } + } + + @Test + void testParseRequireIssuer_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). + requireIssuer(issuer). + parseClaimsJws(compact) + fail() + } catch(MissingClaimException e) { + assertEquals( + String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.ISSUER, issuer), + e.getMessage() + ) + } + } + + @Test + void testParseRequireAudience_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). + requireAudience(audience). + parseClaimsJws(compact) + + assertEquals jwt.getBody().getAudience(), audience + } + + @Test + void testParseRequireAudience_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). + requireAudience(goodAudience). + parseClaimsJws(compact) + fail() + } catch(IncorrectClaimException e) { + assertEquals( + String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.AUDIENCE, goodAudience, badAudience), + e.getMessage() + ) + } + } + + @Test + void testParseRequireAudience_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). + requireAudience(audience). + parseClaimsJws(compact) + fail() + } catch(MissingClaimException e) { + assertEquals( + String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.AUDIENCE, audience), + e.getMessage() + ) + } + } + + @Test + void testParseRequireSubject_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). + requireSubject(subject). + parseClaimsJws(compact) + + assertEquals jwt.getBody().getSubject(), subject + } + + @Test + void testParseRequireSubject_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). + requireSubject(goodSubject). + parseClaimsJws(compact) + fail() + } catch(IncorrectClaimException e) { + assertEquals( + String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.SUBJECT, goodSubject, badSubject), + e.getMessage() + ) + } + } + + @Test + void testParseRequireSubject_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). + requireSubject(subject). + parseClaimsJws(compact) + fail() + } catch(MissingClaimException e) { + assertEquals( + String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.SUBJECT, subject), + e.getMessage() + ) + } + } + + @Test + void testParseRequireId_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). + requireId(id). + parseClaimsJws(compact) + + assertEquals jwt.getBody().getId(), id + } + + @Test + void testParseRequireId_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). + requireId(goodId). + parseClaimsJws(compact) + fail() + } catch(IncorrectClaimException e) { + assertEquals( + String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.ID, goodId, badId), + e.getMessage() + ) + } + } + + @Test + void testParseRequireId_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). + requireId(id). + parseClaimsJws(compact) + fail() + } catch(MissingClaimException e) { + assertEquals( + String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.ID, id), + e.getMessage() + ) + } + } + + @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() + ) + } + } + + @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/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 + } +} 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..161957f0 --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy @@ -0,0 +1,103 @@ +/* + * 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) + } + + @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()) + } + +} 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() {