Skip to content

Commit

Permalink
Merge pull request #475 from auth0/withPayload
Browse files Browse the repository at this point in the history
Add withPayload to JWTCreator.Builder
  • Loading branch information
jimmyjames authored Feb 22, 2021
2 parents bf77ffc + fbbf140 commit a129f53
Show file tree
Hide file tree
Showing 3 changed files with 230 additions and 7 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,16 @@ String token = JWT.create()
.sign(algorithm);
```

You can also create a JWT by calling `withPayload()` and passing a map of claim names to values:

```java
Map<String, Object> payloadClaims = new HashMap<>();
payloadClaims.put("@context", "https://auth0.com/");
String token = JWT.create()
.withPayload(payloadClaims)
.sign(algorithm);
```

You can also verify custom Claims on the `JWT.require()` by calling `withClaim()` and passing both the name and the required value.

```java
Expand Down
57 changes: 53 additions & 4 deletions lib/src/main/java/com/auth0/jwt/JWTCreator.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@
import org.apache.commons.codec.binary.Base64;

import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.Map.Entry;

/**
Expand Down Expand Up @@ -360,6 +357,58 @@ public Builder withClaim(String name, List<?> list) throws IllegalArgumentExcept
return this;
}

/**
* Add specific Claims to set as the Payload. If the provided map is null then
* nothing is changed.
* <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.
* </p>
*
* <p>
* If any of the claims are invalid, none will be added.
* </p>
*
* @param payloadClaims the values to use as Claims in the token's payload.
* @throws IllegalArgumentException if any of the claim keys or null, or if the values are not of a supported type.
* @return this same Builder instance.
*/
public Builder withPayload(Map<String, ?> payloadClaims) throws IllegalArgumentException {
if (payloadClaims == null) {
return this;
}

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

// add claims only after validating all claims so as not to corrupt the claims map of this builder
for (Map.Entry<String, ?> entry : payloadClaims.entrySet()) {
addClaim(entry.getKey(), entry.getValue());
}

return this;
}

private boolean validatePayload(Map<String, ?> payload) {
for (Map.Entry<String, ?> entry : payload.entrySet()) {
String key = entry.getKey();
assertNonNull(key);

Object value = entry.getValue();
if (value instanceof List && !validateClaim((List<?>) value)) {
return false;
} else if (value instanceof Map && !validateClaim((Map<?, ?>) value)) {
return false;
} else if (value != null && !isSupportedType(value)) {
return false;
}
}
return true;
}

private static boolean validateClaim(Map<?, ?> map) {
// do not accept null values in maps
for (Entry<?, ?> entry : map.entrySet()) {
Expand Down
170 changes: 167 additions & 3 deletions lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@
import java.security.interfaces.RSAPrivateKey;
import java.util.*;

import static org.hamcrest.Matchers.anEmptyMap;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
Expand Down Expand Up @@ -717,4 +715,170 @@ public void shouldRefuseCustomListClaimForUnknownArrayType() throws Exception {
.sign(Algorithm.HMAC256("secret"));
}

@Test
public void withPayloadShouldAddBasicClaim() {
Map<String, Object> payload = new HashMap<>();
payload.put("asd", 123);
String jwt = JWTCreator.init()
.withPayload(payload)
.sign(Algorithm.HMAC256("secret"));

assertThat(jwt, is(notNullValue()));
String[] parts = jwt.split("\\.");
String payloadJson = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8);
assertThat(payloadJson, JsonMatcher.hasEntry("asd", 123));
}

@Test
public void withPayloadShouldCreateJwtWithEmptyBodyIfPayloadNull() {
String jwt = JWTCreator.init()
.withPayload(null)
.sign(Algorithm.HMAC256("secret"));

assertThat(jwt, is(notNullValue()));
String[] parts = jwt.split("\\.");
String payloadJson = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8);
assertThat(payloadJson, is("{}"));
}

@Test
public void withPayloadShouldOverwriteExistingClaimIfPayloadMapContainsTheSameKey() {
Map<String, Object> payload = new HashMap<>();
payload.put(PublicClaims.KEY_ID, "xyz");

String jwt = JWTCreator.init()
.withKeyId("abc")
.withPayload(payload)
.sign(Algorithm.HMAC256("secret"));

assertThat(jwt, is(notNullValue()));
String[] parts = jwt.split("\\.");
String payloadJson = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8);
assertThat(payloadJson, JsonMatcher.hasEntry(PublicClaims.KEY_ID, "xyz"));
}

@Test
public void shouldOverwriteExistingPayloadWhenSettingSamePayloadKey() {
Map<String, Object> payload = new HashMap<>();
payload.put(PublicClaims.ISSUER, "xyz");

String jwt = JWTCreator.init()
.withPayload(payload)
.withIssuer("abc")
.sign(Algorithm.HMAC256("secret"));

assertThat(jwt, is(notNullValue()));
String[] parts = jwt.split("\\.");
String payloadJson = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8);
assertThat(payloadJson, JsonMatcher.hasEntry(PublicClaims.ISSUER, "abc"));
}

@Test
public void shouldRemovePayloadIfTheValueIsNull() throws Exception {
String jwt = JWTCreator.init()
.withClaim("key", "stubValue")
.withPayload(Collections.singletonMap("key", (Map<String, ?>) null))
.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);
assertThat(map, anEmptyMap());
}

@Test
public void withPayloadShouldNotAllowCustomType() {
exception.expect(IllegalArgumentException.class);
exception.expectMessage("Claim values must only be of types Map, List, Boolean, Integer, Long, Double, String and Date");

Map<String, Object> payload = new HashMap<>();
payload.put("entry", "value");
payload.put("pojo", new UserPojo("name", 42));
String jwt = JWTCreator.init()
.withPayload(payload)
.sign(Algorithm.HMAC256("secret"));
}

@Test
public void withPayloadShouldAllowNullListItems() {
Map<String, Object> payload = new HashMap<>();
payload.put("list", Arrays.asList("item1", null, "item2"));
String jwt = JWTCreator.init()
.withPayload(payload)
.sign(Algorithm.HMAC256("secret"));

assertThat(jwt, is(notNullValue()));
String[] parts = jwt.split("\\.");
String payloadJson = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8);
assertThat(payloadJson, JsonMatcher.hasEntry("list", Arrays.asList("item1", null, "item2")));
}

@Test
public void withPayloadShouldNotAllowListWithCustomType() {
exception.expect(IllegalArgumentException.class);
exception.expectMessage("Claim values must only be of types Map, List, Boolean, Integer, Long, Double, String and Date");

Map<String, Object> payload = new HashMap<>();
payload.put("list", Arrays.asList("item1", new UserPojo("name", 42)));
String jwt = JWTCreator.init()
.withPayload(payload)
.sign(Algorithm.HMAC256("secret"));
}

@Test
public void withPayloadShouldNotAllowMapWithCustomType() {
exception.expect(IllegalArgumentException.class);
exception.expectMessage("Claim values must only be of types Map, List, Boolean, Integer, Long, Double, String and Date");

Map<String, Object> payload = new HashMap<>();
payload.put("entry", "value");
payload.put("map", Collections.singletonMap("pojo", new UserPojo("name", 42)));
String jwt = JWTCreator.init()
.withPayload(payload)
.sign(Algorithm.HMAC256("secret"));
}

@Test
public void withPayloadShouldAllowNestedSupportedTypes() {
/*
JWT:
{
"stringClaim": "string",
"intClaim": 41,
"listClaim": [
1, 2, {
"nestedObjKey": true
}
],
"objClaim": {
"objKey": ["nestedList1", "nestedList2"]
}
}
*/

List<?> listClaim = Arrays.asList(1, 2, Collections.singletonMap("nestedObjKey", "nestedObjValue"));
Map<String, Object> mapClaim = new HashMap<>();
mapClaim.put("objKey", Arrays.asList("nestedList1", true));

Map<String, Object> payload = new HashMap<>();
payload.put("stringClaim", "string");
payload.put("intClaim", 41);
payload.put("listClaim", listClaim);
payload.put("objClaim", mapClaim);

String jwt = JWTCreator.init()
.withPayload(payload)
.sign(Algorithm.HMAC256("secret"));

assertThat(jwt, is(notNullValue()));
String[] parts = jwt.split("\\.");
String payloadJson = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8);
assertThat(payloadJson, JsonMatcher.hasEntry("stringClaim", "string"));
assertThat(payloadJson, JsonMatcher.hasEntry("intClaim", 41));
assertThat(payloadJson, JsonMatcher.hasEntry("listClaim", listClaim));
assertThat(payloadJson, JsonMatcher.hasEntry("objClaim", mapClaim));
}
}

0 comments on commit a129f53

Please sign in to comment.