Enforce arbitrary claim expectations when parsing

This commit is contained in:
Micah Silverman 2015-09-12 01:09:44 -04:00
parent 5c8c5ef97a
commit 8f49666a40
11 changed files with 450 additions and 2 deletions

View File

@ -16,7 +16,7 @@ Maven:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.5.1</version>
<version>0.6</version>
</dependency>
```
@ -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.

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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.

View File

@ -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);
}
}

View File

@ -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<String, Object> expectedClaims = new LinkedHashMap<String, Object>();
@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
*/

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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<Header, Claims> 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<Header, Claims> 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<Header, Claims> 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<Header,Claims> 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<Header,Claims> 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()
)
}
}
}

View File

@ -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
}
}