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

JWTCreator for basic types #282

Merged
merged 5 commits into from
Feb 13, 2020
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
95 changes: 94 additions & 1 deletion lib/src/main/java/com/auth0/jwt/JWTCreator.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

/**
* The JWTCreator class holds the sign method to generate a complete JWT (with Signature) from a given Header and Payload content.
Expand Down Expand Up @@ -295,14 +297,105 @@ public Builder withArrayClaim(String name, Integer[] items) throws IllegalArgume
* @param name the Claim's name.
* @param items the Claim's value.
* @return this same Builder instance.
* @throws IllegalArgumentException if the name is null.
* @throws IllegalArgumentException if the name is null
*/
public Builder withArrayClaim(String name, Long[] items) throws IllegalArgumentException {
assertNonNull(name);
addClaim(name, items);
return this;
}

/**
* Add a custom Map Claim with the given items.
*
* Accepted nested 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.
*
* @param name the Claim's name.
* @param map the Claim's key-values.
* @return this same Builder instance.
* @throws IllegalArgumentException if the name is null, or if the map contents does not validate.
*/
public Builder withClaim(String name, Map<String, ?> map) throws IllegalArgumentException {
assertNonNull(name);
// validate map contents
if(!validateClaim(map)) {
throw new IllegalArgumentException("Expected map containing Map, List, Boolean, Integer, Long, Double, String and Date");
}
addClaim(name, map);
return this;
}

/**
* Add a custom List Claim with the given items.
*
* Accepted nested 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.
*
* @param name the Claim's name.
* @param list the Claim's list of values.
* @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
if(!validateClaim(list)) {
throw new IllegalArgumentException("Expected list containing Map, List, Boolean, Integer, Long, Double, String and Date");
}
addClaim(name, list);
return this;
}

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)) {
return false;
}

if(entry.getKey() == null || !(entry.getKey() instanceof String)) {
return false;
}
}
return true;
}

private static boolean validateClaim(List<?> list) {
// accept null values in list
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why would we want to accept null values in a list?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The corresponding withArrayClaim(..) does now, so basically it is just the same behaviour.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But are null values so bad? Could leave it up to the caller for value of Map as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking again, it makes sense the way you did it since you're keeping the behavior of the current withArrayClaim implementation that accepts null, and you're not accepting nulls for maps. Let's keep it that way for now.

for (Object object : list) {
if(object != null && !isSupportedType(object)) {
return false;
}
}
return true;
}

private static boolean isSupportedType(Object value) {
if(value instanceof List) {
return validateClaim((List<?>)value);
} else if(value instanceof Map) {
return validateClaim((Map<?, ?>)value);
} else {
return isBasicType(value);
}
}

private static boolean isBasicType(Object value) {
Class<?> c = value.getClass();

if(c.isArray()) {
return c == Integer[].class || c == Long[].class || c == String[].class;
skjolber marked this conversation as resolved.
Show resolved Hide resolved
}
return c == String.class || c == Integer.class || c == Long.class || c == Double.class || c == Date.class || c == Boolean.class;
}

/**
* Creates a new JWT and signs is with the given algorithm
*
Expand Down
226 changes: 225 additions & 1 deletion lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import com.auth0.jwt.impl.PublicClaims;
import com.auth0.jwt.interfaces.ECDSAKeyProvider;
import com.auth0.jwt.interfaces.RSAKeyProvider;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.apache.commons.codec.binary.Base64;
import org.junit.Rule;
import org.junit.Test;
Expand All @@ -12,9 +14,12 @@
import java.nio.charset.StandardCharsets;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.RSAPrivateKey;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.hamcrest.Matchers.is;
Expand Down Expand Up @@ -431,4 +436,223 @@ public void shouldAcceptCustomArrayClaimOfTypeLong() throws Exception {
String[] parts = jwt.split("\\.");
assertThat(parts[1], is("eyJuYW1lIjpbMSwyLDNdfQ"));
}
}

@Test
public void shouldAcceptCustomClaimOfTypeMap() throws Exception {
Map<String, Object> data = new HashMap<>();
data.put("test1", "abc");
data.put("test2", "def");
String jwt = JWTCreator.init()
.withClaim("data", data)
.sign(Algorithm.HMAC256("secret"));

assertThat(jwt, is(notNullValue()));
String[] parts = jwt.split("\\.");
assertThat(parts[1], is("eyJkYXRhIjp7InRlc3QyIjoiZGVmIiwidGVzdDEiOiJhYmMifX0"));
}

@Test
public void shouldRefuseCustomClaimOfTypeUserPojo() throws Exception{
Map<String, Object> data = new HashMap<>();
data.put("test1", new UserPojo("Michael", 255));

exception.expect(IllegalArgumentException.class);

JWTCreator.init()
.withClaim("pojo", data)
.sign(Algorithm.HMAC256("secret"));
}

@SuppressWarnings("unchecked")
@Test
public void shouldAcceptCustomMapClaimOfBasicObjectTypes() throws Exception {
Map<String, Object> data = new HashMap<>();

// simple types
data.put("string", "abc");
data.put("integer", 1);
data.put("long", Long.MAX_VALUE);
data.put("double", 123.456d);
data.put("date", new Date(123L));
data.put("boolean", true);

// array types
data.put("intArray", new Integer[]{3, 5});
data.put("longArray", new Long[]{Long.MAX_VALUE, Long.MIN_VALUE});
data.put("stringArray", new String[]{"string"});

data.put("list", Arrays.asList("a", "b", "c"));

Map<String, Object> sub = new HashMap<>();
sub.put("subKey", "subValue");

data.put("map", sub);

String jwt = JWTCreator.init()
.withClaim("data", data)
.sign(Algorithm.HMAC256("secret"));

assertThat(jwt, is(notNullValue()));
String[] parts = jwt.split("\\.");

String body = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8);
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> map = (Map<String, Object>) mapper.readValue(body, Map.class).get("data");

assertThat(map.get("string"), is("abc"));
assertThat(map.get("integer"), is(1));
assertThat(map.get("long"), is(Long.MAX_VALUE));
assertThat(map.get("double"), is(123.456d));
assertThat(map.get("date"), is(123));
assertThat(map.get("boolean"), is(true));

// array types
assertThat(map.get("intArray"), is(Arrays.asList(new Integer[]{3, 5})));
assertThat(map.get("longArray"), is(Arrays.asList(new Long[]{Long.MAX_VALUE, Long.MIN_VALUE})));
assertThat(map.get("stringArray"), is(Arrays.asList(new String[]{"string"})));

// list
assertThat(map.get("list"), is(Arrays.asList("a", "b", "c")));
assertThat(map.get("map"), is(sub));

}

@SuppressWarnings("unchecked")
@Test
public void shouldAcceptCustomListClaimOfBasicObjectTypes() throws Exception {
List<Object> data = new ArrayList<>();

// simple types
data.add("abc");
data.add(1);
data.add(Long.MAX_VALUE);
data.add(123.456d);
data.add(new Date(123L));
data.add(true);

// array types
data.add(new Integer[]{3, 5});
data.add(new Long[]{Long.MAX_VALUE, Long.MIN_VALUE});
data.add(new String[]{"string"});

data.add(Arrays.asList("a", "b", "c"));

Map<String, Object> sub = new HashMap<>();
sub.put("subKey", "subValue");

data.add(sub);

String jwt = JWTCreator.init()
.withClaim("data", data)
.sign(Algorithm.HMAC256("secret"));

assertThat(jwt, is(notNullValue()));
String[] parts = jwt.split("\\.");

String body = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8);
ObjectMapper mapper = new ObjectMapper();
List<Object> list = (List<Object>) mapper.readValue(body, Map.class).get("data");

assertThat(list.get(0), is("abc"));
assertThat(list.get(1), is(1));
assertThat(list.get(2), is(Long.MAX_VALUE));
assertThat(list.get(3), is(123.456d));
assertThat(list.get(4), is(123));
assertThat(list.get(5), is(true));

// array types
assertThat(list.get(6), is(Arrays.asList(new Integer[]{3, 5})));
assertThat(list.get(7), is(Arrays.asList(new Long[]{Long.MAX_VALUE, Long.MIN_VALUE})));
assertThat(list.get(8), is(Arrays.asList(new String[]{"string"})));

// list
assertThat(list.get(9), is(Arrays.asList("a", "b", "c")));
assertThat(list.get(10), is(sub));

}

@Test
public void shouldAcceptCustomClaimForNullListItem() throws Exception{
Map<String, Object> data = new HashMap<>();
data.put("test1", Arrays.asList("a", null, "c"));

JWTCreator.init()
.withClaim("pojo", data)
.sign(Algorithm.HMAC256("secret"));
}

@Test
public void shouldRefuseCustomClaimForNullMapValue() throws Exception{
Map<String, Object> data = new HashMap<>();
data.put("subKey", null);

exception.expect(IllegalArgumentException.class);

JWTCreator.init()
.withClaim("pojo", data)
.sign(Algorithm.HMAC256("secret"));
}

@Test
public void shouldRefuseCustomClaimForNullMapKey() throws Exception{
Map<String, Object> data = new HashMap<>();
data.put(null, "subValue");

exception.expect(IllegalArgumentException.class);

JWTCreator.init()
.withClaim("pojo", data)
.sign(Algorithm.HMAC256("secret"));
}

@SuppressWarnings({ "unchecked", "rawtypes" })
@Test
public void shouldRefuseCustomMapClaimForNonStringKey() throws Exception{
Map data = new HashMap<>();
data.put(new Object(), "value");

exception.expect(IllegalArgumentException.class);

JWTCreator.init()
.withClaim("pojo", (Map<String, Object>)data)
.sign(Algorithm.HMAC256("secret"));
}

@Test
public void shouldRefuseCustomListClaimForUnknownListElement() throws Exception{
List<Object> list = Arrays.asList(new UserPojo("Michael", 255));

exception.expect(IllegalArgumentException.class);

JWTCreator.init()
.withClaim("list", list)
.sign(Algorithm.HMAC256("secret"));
}

@Test
public void shouldRefuseCustomListClaimForUnknownListElementWrappedInAMap() throws Exception{
List<Object> list = Arrays.asList(new UserPojo("Michael", 255));

Map<String, Object> data = new HashMap<>();
data.put("someList", list);

exception.expect(IllegalArgumentException.class);

JWTCreator.init()
.withClaim("list", list)
.sign(Algorithm.HMAC256("secret"));
}

@Test
public void shouldRefuseCustomListClaimForUnknownArrayType() throws Exception{
List<Object> list = new ArrayList<>();
list.add(new Object[] {"test"});

exception.expect(IllegalArgumentException.class);

JWTCreator.init()
.withClaim("list", list)
.sign(Algorithm.HMAC256("secret"));
}

}