Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SDK-3158] Null claim handling #564

Merged
merged 17 commits into from
Mar 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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