Skip to content

Commit

Permalink
[SDK-3158] Null claim handling (#564)
Browse files Browse the repository at this point in the history
* Handle claim difference between missing and null

* Verification with null claims

* JWT creation support for null values

* Test cases for JWT verification and construction

* Add JWT decode test cases

* Fix broken tests

* Fixed Lint issues

* Fixed formatting errors

* Add test case to check Claim toString conversion
  • Loading branch information
poovamraj authored Mar 25, 2022
1 parent 0029a63 commit e37301a
Show file tree
Hide file tree
Showing 15 changed files with 273 additions and 281 deletions.
45 changes: 28 additions & 17 deletions lib/src/main/java/com/auth0/jwt/JWTCreator.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ public static class Builder {
/**
* Add specific Claims to set as the Header.
* If provided map is null then nothing is changed
* If provided map contains a claim with null value then that claim will be removed from the header
*
* @param headerClaims the values to use as Claims in the token's Header.
* @return this same Builder instance.
Expand Down Expand Up @@ -362,7 +361,6 @@ public Builder withClaim(String name, Map<String, ?> map) throws IllegalArgument
* @return this same Builder instance.
* @throws IllegalArgumentException if the name is null, or if the list contents does not validate.
*/

public Builder withClaim(String name, List<?> list) throws IllegalArgumentException {
assertNonNull(name);
// validate list contents
Expand All @@ -374,6 +372,19 @@ public Builder withClaim(String name, List<?> list) throws IllegalArgumentExcept
return this;
}

/**
* Add a custom claim with null value.
*
* @param name the Claim's name.
* @return this same Builder instance.
* @throws IllegalArgumentException if the name is null
*/
public Builder withNullClaim(String name) throws IllegalArgumentException {
assertNonNull(name);
addClaim(name, null);
return this;
}

/**
* Add a custom Array Claim with the given items.
*
Expand Down Expand Up @@ -422,8 +433,8 @@ public Builder withArrayClaim(String name, Long[] items) throws IllegalArgumentE
* <p>
* Accepted types are {@linkplain Map} and {@linkplain List} with basic types
* {@linkplain Boolean}, {@linkplain Integer}, {@linkplain Long}, {@linkplain Double},
* {@linkplain String} and {@linkplain Date}. {@linkplain Map}s cannot contain null keys or values.
* {@linkplain List}s can contain null elements.
* {@linkplain String} and {@linkplain Date}.
* {@linkplain Map}s and {@linkplain List}s can contain null elements.
* </p>
*
* <p>
Expand All @@ -442,7 +453,7 @@ public Builder withPayload(Map<String, ?> payloadClaims) throws IllegalArgumentE

if (!validatePayload(payloadClaims)) {
throw new IllegalArgumentException("Claim values must only be of types Map, List, Boolean, Integer, "
+ "Long, Double, String and Date");
+ "Long, Double, String, Date and Null");
}

// add claims only after validating all claims so as not to corrupt the claims map of this builder
Expand All @@ -463,7 +474,7 @@ private boolean validatePayload(Map<String, ?> payload) {
return false;
} else if (value instanceof Map && !validateClaim((Map<?, ?>) value)) {
return false;
} else if (value != null && !isSupportedType(value)) {
} else if (!isSupportedType(value)) {
return false;
}
}
Expand All @@ -474,7 +485,7 @@ private static boolean validateClaim(Map<?, ?> map) {
// do not accept null values in maps
for (Entry<?, ?> entry : map.entrySet()) {
Object value = entry.getValue();
if (value == null || !isSupportedType(value)) {
if (!isSupportedType(value)) {
return false;
}

Expand All @@ -488,7 +499,7 @@ private static boolean validateClaim(Map<?, ?> map) {
private static boolean validateClaim(List<?> list) {
// accept null values in list
for (Object object : list) {
if (object != null && !isSupportedType(object)) {
if (!isSupportedType(object)) {
return false;
}
}
Expand All @@ -506,13 +517,17 @@ private static boolean isSupportedType(Object value) {
}

private static boolean isBasicType(Object value) {
Class<?> c = value.getClass();
if (value == null) {
return true;
} else {
Class<?> c = value.getClass();

if (c.isArray()) {
return c == Integer[].class || c == Long[].class || c == String[].class;
if (c.isArray()) {
return c == Integer[].class || c == Long[].class || c == String[].class;
}
return c == String.class || c == Integer.class || c == Long.class || c == Double.class
|| c == Date.class || c == Instant.class || c == Boolean.class;
}
return c == String.class || c == Integer.class || c == Long.class || c == Double.class
|| c == Date.class || c == Instant.class || c == Boolean.class;
}

/**
Expand Down Expand Up @@ -546,10 +561,6 @@ private void assertNonNull(String name) {
}

private void addClaim(String name, Object value) {
if (value == null) {
payloadClaims.remove(name);
return;
}
payloadClaims.put(name, value);
}
}
Expand Down
13 changes: 10 additions & 3 deletions lib/src/main/java/com/auth0/jwt/JWTVerifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.*;
import com.auth0.jwt.impl.JWTParser;
import com.auth0.jwt.impl.NullClaim;
import com.auth0.jwt.impl.PublicClaims;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
Expand Down Expand Up @@ -146,14 +145,21 @@ public Verification withJWTId(String jwtId) {
public Verification withClaimPresence(String name) throws IllegalArgumentException {
assertNonNull(name);
withClaim(name, ((claim, decodedJWT) -> {
if (claim instanceof NullClaim) {
if (claim.isMissing()) {
throw new InvalidClaimException(String.format("The Claim '%s' is not present in the JWT.", name));
}
return true;
}));
return this;
}

@Override
public Verification withNullClaim(String name) throws IllegalArgumentException {
assertNonNull(name);
withClaim(name, ((claim, decodedJWT) -> claim.isNull()));
return this;
}

@Override
public Verification withClaim(String name, Boolean value) throws IllegalArgumentException {
assertNonNull(name);
Expand Down Expand Up @@ -292,7 +298,8 @@ private boolean assertValidCollectionClaim(Claim claim, Object[] expectedClaimVa
}
}
} else {
claimArr = claim.isNull() ? Collections.emptyList() : Arrays.asList(claim.as(Object[].class));
claimArr = claim.isNull() || claim.isMissing()
? Collections.emptyList() : Arrays.asList(claim.as(Object[].class));
}
List<Object> valueArr = Arrays.asList(expectedClaimValue);
return claimArr.containsAll(valueArr);
Expand Down
39 changes: 25 additions & 14 deletions lib/src/main/java/com/auth0/jwt/impl/JsonNodeClaim.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,32 +31,32 @@ private JsonNodeClaim(JsonNode node, ObjectReader objectReader) {

@Override
public Boolean asBoolean() {
return !data.isBoolean() ? null : data.asBoolean();
return isMissing() || isNull() || !data.isBoolean() ? null : data.asBoolean();
}

@Override
public Integer asInt() {
return !data.isNumber() ? null : data.asInt();
return isMissing() || isNull() || !data.isNumber() ? null : data.asInt();
}

@Override
public Long asLong() {
return !data.isNumber() ? null : data.asLong();
return isMissing() || isNull() || !data.isNumber() ? null : data.asLong();
}

@Override
public Double asDouble() {
return !data.isNumber() ? null : data.asDouble();
return isMissing() || isNull() || !data.isNumber() ? null : data.asDouble();
}

@Override
public String asString() {
return !data.isTextual() ? null : data.asText();
return isMissing() || isNull() || !data.isTextual() ? null : data.asText();
}

@Override
public Date asDate() {
if (!data.canConvertToLong()) {
if (isMissing() || isNull() || !data.canConvertToLong()) {
return null;
}
long seconds = data.asLong();
Expand All @@ -65,7 +65,7 @@ public Date asDate() {

@Override
public Instant asInstant() {
if (!data.canConvertToLong()) {
if (isMissing() || isNull() || !data.canConvertToLong()) {
return null;
}
long seconds = data.asLong();
Expand All @@ -75,7 +75,7 @@ public Instant asInstant() {
@Override
@SuppressWarnings("unchecked")
public <T> T[] asArray(Class<T> clazz) throws JWTDecodeException {
if (!data.isArray()) {
if (isMissing() || isNull() || !data.isArray()) {
return null;
}

Expand All @@ -92,7 +92,7 @@ public <T> T[] asArray(Class<T> clazz) throws JWTDecodeException {

@Override
public <T> List<T> asList(Class<T> clazz) throws JWTDecodeException {
if (!data.isArray()) {
if (isMissing() || isNull() || !data.isArray()) {
return null;
}

Expand All @@ -109,7 +109,7 @@ public <T> List<T> asList(Class<T> clazz) throws JWTDecodeException {

@Override
public Map<String, Object> asMap() throws JWTDecodeException {
if (!data.isObject()) {
if (isMissing() || isNull() || !data.isObject()) {
return null;
}

Expand All @@ -126,6 +126,9 @@ public Map<String, Object> asMap() throws JWTDecodeException {
@Override
public <T> T as(Class<T> clazz) throws JWTDecodeException {
try {
if (isMissing() || isNull()) {
return null;
}
return objectReader.treeAsTokens(data).readValueAs(clazz);
} catch (IOException e) {
throw new JWTDecodeException("Couldn't map the Claim value to " + clazz.getSimpleName(), e);
Expand All @@ -134,11 +137,21 @@ public <T> T as(Class<T> clazz) throws JWTDecodeException {

@Override
public boolean isNull() {
return false;
return !isMissing() && data.isNull();
}

@Override
public boolean isMissing() {
return data == null || data.isMissingNode();
}

@Override
public String toString() {
if (isMissing()) {
return "Missing claim";
} else if (isNull()) {
return "Null claim";
}
return data.toString();
}

Expand All @@ -161,10 +174,8 @@ static Claim extractClaim(String claimName, Map<String, JsonNode> tree, ObjectRe
* @return a valid Claim instance. If the node is null or missing, a NullClaim will be returned.
*/
static Claim claimFromNode(JsonNode node, ObjectReader objectReader) {
if (node == null || node.isNull() || node.isMissingNode()) {
return new NullClaim();
}
return new JsonNodeClaim(node, objectReader);
}

}
//todo test all as* methods in JsonNodeClaim to ensure isMissing isNull calls are made
79 changes: 0 additions & 79 deletions lib/src/main/java/com/auth0/jwt/impl/NullClaim.java

This file was deleted.

10 changes: 10 additions & 0 deletions lib/src/main/java/com/auth0/jwt/interfaces/Claim.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,20 @@ public interface Claim {

/**
* Whether this Claim has a null value or not.
* If the claim is not present, it will return false hence checking {@link Claim#isMissing} is advised as well
*
* @return whether this Claim has a null value or not.
*/
boolean isNull();

/**
* Can be used to verify whether the Claim is found or not.
* This will be true even if the Claim has null value associated to it.
*
* @return whether this Claim is present or not
*/
boolean isMissing();

/**
* Get this Claim as a Boolean.
* If the value isn't of type Boolean or it can't be converted to a Boolean, null will be returned.
Expand Down Expand Up @@ -110,6 +119,7 @@ default Instant asInstant() {

/**
* Get this Claim as a custom type T.
* This method will return null if {@link Claim#isMissing()} or {@link Claim#isNull()} is true
*
* @param <T> type
* @param clazz the type class
Expand Down
Loading

0 comments on commit e37301a

Please sign in to comment.